diff --git a/chat/CLAUDE.md b/chat/CLAUDE.md
new file mode 100644
index 000000000..63fe17c3c
--- /dev/null
+++ b/chat/CLAUDE.md
@@ -0,0 +1,122 @@
+# Chat Project Guide
+
+## Project Structure
+
+```
+chat/
+├── apps/
+│ ├── landing/ # Astro marketing landing page (@chat/landing)
+│ ├── web/ # SvelteKit web application (@chat/web)
+│ └── mobile/ # Expo/React Native mobile app (@chat/mobile)
+├── backend/ # NestJS API server (@chat/backend)
+├── packages/
+│ └── chat-types/ # Shared TypeScript types (@chat/types)
+└── package.json
+```
+
+## Commands
+
+### Root Level
+```bash
+pnpm chat:dev # Run all chat apps
+pnpm dev:chat:mobile # Start mobile app
+pnpm dev:chat:web # Start web app
+pnpm dev:chat:landing # Start landing page
+pnpm dev:chat:backend # Start backend server
+```
+
+### Mobile App (chat/apps/mobile)
+```bash
+pnpm dev # Start Expo dev server
+pnpm ios # Run on iOS simulator
+pnpm android # Run on Android emulator
+pnpm build:dev # Build development version
+pnpm build:preview # Build preview version
+pnpm build:prod # Build production version
+```
+
+### Backend (chat/backend)
+```bash
+pnpm start:dev # Start with hot reload
+pnpm build # Build for production
+pnpm start:prod # Start production server
+```
+
+### Web App (chat/apps/web)
+```bash
+pnpm dev # Start dev server
+pnpm build # Build for production
+pnpm preview # Preview production build
+```
+
+### Landing Page (chat/apps/landing)
+```bash
+pnpm dev # Start dev server
+pnpm build # Build for production
+pnpm preview # Preview production build
+```
+
+## Technology Stack
+
+- **Mobile**: React Native 0.76.7 + Expo SDK 52, NativeWind, Expo Router
+- **Web**: SvelteKit 2.x, Svelte 5, Tailwind CSS 4
+- **Landing**: Astro 5.16, Tailwind CSS
+- **Backend**: NestJS 10, Azure OpenAI, Supabase
+- **Types**: TypeScript 5.x
+
+## Architecture
+
+### Backend API Endpoints
+
+| Endpoint | Method | Description |
+|----------|--------|-------------|
+| `/api/health` | GET | Health check |
+| `/api/chat/models` | GET | List available AI models |
+| `/api/chat/completions` | POST | Create chat completion |
+| `/api/conversations` | GET | List user conversations |
+| `/api/conversations/:id` | GET | Get conversation details |
+| `/api/conversations/:id/messages` | GET | Get conversation messages |
+| `/api/conversations` | POST | Create new conversation |
+| `/api/conversations/:id/messages` | POST | Add message to conversation |
+
+### Environment Variables
+
+#### Backend (.env)
+```
+AZURE_OPENAI_ENDPOINT=https://...
+AZURE_OPENAI_API_KEY=...
+AZURE_OPENAI_API_VERSION=2024-12-01-preview
+SUPABASE_URL=https://...
+SUPABASE_SERVICE_KEY=...
+PORT=3001
+```
+
+#### Mobile (.env)
+```
+EXPO_PUBLIC_SUPABASE_URL=https://...
+EXPO_PUBLIC_SUPABASE_ANON_KEY=...
+EXPO_PUBLIC_BACKEND_URL=http://localhost:3001
+```
+
+## Code Style Guidelines
+
+- **TypeScript**: Strict typing with interfaces
+- **Mobile**: Functional components with hooks
+- **Web**: Svelte 5 runes mode
+- **Styling**: Tailwind CSS everywhere
+- **Formatting**: 100 char line limit, 2 space tabs, single quotes
+
+## AI Models Available
+
+| Model ID | Name | Description |
+|----------|------|-------------|
+| 550e8400-e29b-41d4-a716-446655440000 | GPT-O3-Mini | Fast, efficient responses |
+| 550e8400-e29b-41d4-a716-446655440004 | GPT-4o-Mini | Compact, powerful |
+| 550e8400-e29b-41d4-a716-446655440005 | GPT-4o | Most advanced |
+
+## Important Notes
+
+1. **Security**: API keys are stored in the backend only - never in client apps
+2. **Authentication**: Uses Supabase Auth, shared with Mana Core ecosystem
+3. **Database**: Supabase PostgreSQL with RLS policies
+4. **Deployment**: Backend runs on port 3001 by default
diff --git a/chat/apps/landing/astro.config.mjs b/chat/apps/landing/astro.config.mjs
new file mode 100644
index 000000000..689f23fdb
--- /dev/null
+++ b/chat/apps/landing/astro.config.mjs
@@ -0,0 +1,11 @@
+import { defineConfig } from 'astro/config';
+import tailwind from '@astrojs/tailwind';
+import sitemap from '@astrojs/sitemap';
+
+export default defineConfig({
+ site: 'https://chat.manacore.app',
+ integrations: [
+ tailwind(),
+ sitemap()
+ ]
+});
diff --git a/chat/apps/landing/package.json b/chat/apps/landing/package.json
new file mode 100644
index 000000000..f7286504f
--- /dev/null
+++ b/chat/apps/landing/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@chat/landing",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "astro dev",
+ "start": "astro dev",
+ "build": "astro check && astro build",
+ "preview": "astro preview",
+ "astro": "astro",
+ "type-check": "astro check"
+ },
+ "dependencies": {
+ "@astrojs/check": "^0.9.0",
+ "@astrojs/sitemap": "^3.2.1",
+ "@manacore/shared-landing-ui": "workspace:*",
+ "astro": "^5.16.0",
+ "typescript": "^5.0.0"
+ },
+ "devDependencies": {
+ "@astrojs/tailwind": "^6.0.0",
+ "@tailwindcss/typography": "^0.5.16",
+ "tailwindcss": "^3.4.17"
+ }
+}
diff --git a/chat/apps/landing/src/layouts/BaseLayout.astro b/chat/apps/landing/src/layouts/BaseLayout.astro
new file mode 100644
index 000000000..6ddbd1850
--- /dev/null
+++ b/chat/apps/landing/src/layouts/BaseLayout.astro
@@ -0,0 +1,28 @@
+---
+interface Props {
+ title: string;
+ description?: string;
+}
+
+const { title, description = 'ManaChat - AI Chat Assistant' } = Astro.props;
+---
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
diff --git a/chat/apps/landing/src/pages/index.astro b/chat/apps/landing/src/pages/index.astro
new file mode 100644
index 000000000..ba96864c4
--- /dev/null
+++ b/chat/apps/landing/src/pages/index.astro
@@ -0,0 +1,100 @@
+---
+import BaseLayout from '../layouts/BaseLayout.astro';
+---
+
+
+
+
+
+
+
+ ManaChat
+
+
+ Dein intelligenter KI-Chat-Assistent
+
+
+ Chatte mit den leistungsstärksten KI-Modellen. GPT-4o, GPT-4o-Mini und mehr -
+ alles in einer einfachen, eleganten Oberfläche.
+
+
+
+
+
+
+
+
+
+ Funktionen
+
+
+
+
🤖
+
+ Mehrere KI-Modelle
+
+
+ Wähle zwischen GPT-4o, GPT-4o-Mini und weiteren Modellen für deine Gespräche.
+
+
+
+
💬
+
+ Konversationen speichern
+
+
+ Alle deine Chats werden sicher gespeichert und sind jederzeit abrufbar.
+
+
+
+
📱
+
+ Plattformübergreifend
+
+
+ Nutze ManaChat auf iOS, Android und im Web - deine Daten sind überall synchronisiert.
+
+
+
+
+
+
+
+
+
+
+ Bereit für intelligente Gespräche?
+
+
+ Starte jetzt kostenlos mit ManaChat.
+
+
+ Jetzt herunterladen
+
+
+
+
+
+
+
+
diff --git a/chat/apps/landing/tailwind.config.mjs b/chat/apps/landing/tailwind.config.mjs
new file mode 100644
index 000000000..c751e832a
--- /dev/null
+++ b/chat/apps/landing/tailwind.config.mjs
@@ -0,0 +1,28 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
+ theme: {
+ extend: {
+ colors: {
+ primary: {
+ 50: '#eff6ff',
+ 100: '#dbeafe',
+ 200: '#bfdbfe',
+ 300: '#93c5fd',
+ 400: '#60a5fa',
+ 500: '#0A84FF',
+ 600: '#2563eb',
+ 700: '#1d4ed8',
+ 800: '#1e40af',
+ 900: '#1e3a8a',
+ },
+ secondary: {
+ 500: '#5E5CE6',
+ }
+ }
+ },
+ },
+ plugins: [
+ require('@tailwindcss/typography'),
+ ],
+};
diff --git a/chat/apps/landing/tsconfig.json b/chat/apps/landing/tsconfig.json
new file mode 100644
index 000000000..c5450d304
--- /dev/null
+++ b/chat/apps/landing/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ }
+}
diff --git a/chat/apps/mobile/.env.example b/chat/apps/mobile/.env.example
new file mode 100644
index 000000000..ff93a8b32
--- /dev/null
+++ b/chat/apps/mobile/.env.example
@@ -0,0 +1,7 @@
+# Supabase Konfiguration
+EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
+EXPO_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
+
+# Chat Backend API
+# The backend handles AI API calls securely - no API keys needed in the mobile app
+EXPO_PUBLIC_BACKEND_URL=http://localhost:3001
diff --git a/chat/apps/mobile/.gitignore b/chat/apps/mobile/.gitignore
new file mode 100644
index 000000000..1861e0868
--- /dev/null
+++ b/chat/apps/mobile/.gitignore
@@ -0,0 +1,25 @@
+node_modules/
+.expo/
+dist/
+npm-debug.*
+*.jks
+*.p8
+*.p12
+*.key
+*.mobileprovision
+*.orig.*
+web-build/
+# expo router
+expo-env.d.ts
+
+# firebase/supabase/vexo
+.env
+
+ios
+android
+
+# macOS
+.DS_Store
+
+# Temporary files created by Metro to check the health of the file watcher
+.metro-health-check*
\ No newline at end of file
diff --git a/chat/apps/mobile/CLAUDE.md b/chat/apps/mobile/CLAUDE.md
new file mode 100644
index 000000000..7b6d8928f
--- /dev/null
+++ b/chat/apps/mobile/CLAUDE.md
@@ -0,0 +1,52 @@
+# Claude's Guide to Chat Mobile App
+
+## Commands
+- Start app: `pnpm dev` or `pnpm start`
+- iOS: `pnpm ios`
+- Android: `pnpm android`
+- Lint: `pnpm lint`
+- Format: `pnpm format`
+- Build: `pnpm build:dev`, `pnpm build:preview`, `pnpm build:prod`
+- Supabase: `pnpm supabase:cli`, `pnpm supabase:update-models`, `pnpm supabase:setup`
+
+## Architecture
+
+### Backend Integration
+- **AI API calls go through the backend** - NOT directly from the mobile app
+- Backend URL configured via `EXPO_PUBLIC_BACKEND_URL` environment variable
+- API keys are stored securely in the backend only
+- `utils/backendApi.ts` - Backend client for AI completions
+- `utils/api.ts` - API wrapper that routes calls to backend
+
+### Key Files
+- `config/azure.ts` - Model definitions (NO API keys!)
+- `services/openai.ts` - Chat service using backend
+- `utils/backendApi.ts` - Backend API client
+- `utils/supabase.ts` - Supabase client for data persistence
+
+## Code Style Guidelines
+- **TypeScript**: Strict typing with interfaces for props and state
+- **Components**: Functional components with hooks, located in `/components`
+- **Navigation**: Expo Router in `/app` directory
+- **Styling**: NativeWind (Tailwind CSS for React Native)
+- **Imports**: Path aliases with `~/*` for project root
+- **Formatting**: 100 char line limit, 2 space tabs, single quotes
+- **State**: React Context API for global state
+- **Backend**: Uses NestJS backend for AI calls, Supabase for data
+- **Naming**: PascalCase for components, camelCase for functions/variables
+- **Error Handling**: Try/catch with contextual error messages
+
+## Environment Variables
+
+```
+EXPO_PUBLIC_SUPABASE_URL=https://...
+EXPO_PUBLIC_SUPABASE_ANON_KEY=...
+EXPO_PUBLIC_BACKEND_URL=http://localhost:3001
+```
+
+## Running with Backend
+
+1. Start the backend first: `pnpm dev:chat:backend`
+2. Then start the mobile app: `pnpm dev:chat:mobile`
+
+The mobile app will connect to the backend for AI completions.
diff --git a/chat/apps/mobile/README.md b/chat/apps/mobile/README.md
new file mode 100644
index 000000000..3751d20f8
--- /dev/null
+++ b/chat/apps/mobile/README.md
@@ -0,0 +1,63 @@
+# Chat App
+
+Eine moderne mobile Chat-Anwendung zur Interaktion mit verschiedenen KI-Sprachmodellen.
+
+## Funktionen
+
+- 💬 Chat mit verschiedenen KI-Modellen (GPT-4, GPT-3.5, Claude 3)
+- 🔄 Verschiedene Konversationsmodi (frei, geführt, vorlagenbasiert)
+- 👤 Benutzerauthentifizierung (Registrierung, Anmeldung, Passwort-Reset)
+- 📱 Cross-Platform (iOS, Android, Web) mit Expo
+- 🎨 Modernes UI mit NativeWind (Tailwind CSS)
+
+## Technologie-Stack
+
+- **Frontend:** React Native mit Expo SDK 52
+- **Routing:** Expo Router v4
+- **Styling:** NativeWind (Tailwind CSS)
+- **Backend:** Supabase (Auth, PostgreSQL)
+- **API:** Azure OpenAI API
+
+## Einrichtung
+
+1. Repository klonen
+ ```
+ git clone
+ cd chat
+ ```
+
+2. Abhängigkeiten installieren
+ ```
+ npm install
+ ```
+
+3. Umgebungsvariablen konfigurieren
+ ```
+ cp .env.example .env
+ ```
+ Dann `.env` mit deinen Supabase- und Azure OpenAI-Zugangsdaten bearbeiten.
+
+4. Entwicklungsserver starten
+ ```
+ npm run start
+ ```
+
+## Projektstruktur
+
+- `/app` - Hauptanwendungslogik (Expo Router)
+- `/components` - Wiederverwendbare UI-Komponenten
+- `/services` - Business-Logik und API-Dienste
+- `/utils` - Hilfsfunktionen
+- `/context` - React Context Provider
+
+## Nutzung
+
+Nach dem Start kannst du:
+- Dich registrieren oder anmelden
+- Ein KI-Modell auswählen
+- Eine neue Konversation starten
+- Zwischen verschiedenen Konversationsmodi wechseln
+
+## Lizenz
+
+MIT
\ No newline at end of file
diff --git a/chat/apps/mobile/VEREINFACHUNG.md b/chat/apps/mobile/VEREINFACHUNG.md
new file mode 100644
index 000000000..cb707c8e6
--- /dev/null
+++ b/chat/apps/mobile/VEREINFACHUNG.md
@@ -0,0 +1,55 @@
+# Vereinfachungsplan für Chat App
+
+Basierend auf der Codeanalyse schlage ich folgende Maßnahmen zur Vereinfachung des Projekts vor:
+
+## 1. Komponenten-Konsolidierung
+
+- **Chat-Eingabefelder**: `MessageInput.tsx` und `ChatPromptInput.tsx` zu einer Komponente zusammenführen
+- **Modell-Auswahl**: Die Logik aus `ModelDropdown.tsx` und `model-selection.tsx` in einen gemeinsamen Service extrahieren
+- **Nachrichten-Darstellung**: Eine wiederverwendbare `MessageRenderer`-Komponente für alle Nachrichten-Displaytypen erstellen
+
+## 2. Code-Reduktion
+
+- **Redundante Modell-Definitionen**: Gemeinsame Typendefinitionen in `types/index.ts` zentralisieren
+- **API-Wrapper**: XHR durch einfachen Fetch-API-Wrapper in `utils/api.ts` ersetzen
+- **Error Handling**: Zentrales Fehlerbehandlungssystem statt wiederholter try/catch-Blöcke
+- **Styling**: Vollständig auf NativeWind umstellen und StyleSheet.create entfernen
+
+## 3. Architektur-Optimierung
+
+- **State Management**:
+ - Auth-Zustand über einen zentralen Store verwalten
+ - Modell- und Konversationszustand aus UI-Komponenten in Services verlagern
+
+- **Typ-System**:
+ - Gemeinsame Schnittstellen für Modelle, Nachrichten und Konversationen
+ - Striktere Typprüfung für alle API-Antworten
+
+- **Service-Layer**:
+ - Klare Trennung zwischen UI, Datenmodell und API-Logik
+ - Einheitliche Fehlerrückgabe mit Typisierung
+
+## 4. Dateistruktur
+
+```
+/app - Screens & Routing
+/components - UI-Komponenten
+/hooks - Gemeinsame React Hooks
+/services - Business-Logik
+/types - Typendefinitionen
+/utils - Hilfsfunktionen
+```
+
+## 5. Performance-Optimierungen
+
+- Virtualisierte Listen für große Nachrichtenthreads
+- Optimistische UI-Updates für bessere UX
+- Caching von Modellantworten zur Reduzierung von API-Aufrufen
+
+## Implementierungsreihenfolge
+
+1. Typensystem konsolidieren
+2. API-Wrapper erstellen
+3. State Management umstellen
+4. UI-Komponenten vereinheitlichen
+5. Styling standardisieren
\ No newline at end of file
diff --git a/chat/apps/mobile/VEREINFACHUNG_STATUS.md b/chat/apps/mobile/VEREINFACHUNG_STATUS.md
new file mode 100644
index 000000000..3c11b787b
--- /dev/null
+++ b/chat/apps/mobile/VEREINFACHUNG_STATUS.md
@@ -0,0 +1,38 @@
+# Vereinfachungsplan: Status
+
+Fortschritt bei der Umsetzung des Vereinfachungsplans:
+
+## ✅ Zentrale Typendefinitionen
+- Typendefinitionen für Message, Model, Conversation, etc. in `/types/index.ts` erstellt
+- Stellt sicher, dass alle Komponenten die gleichen Typen verwenden
+
+## ✅ API-Wrapper
+- Modern `fetch`-basierter API-Wrapper in `/utils/api.ts` erstellt
+- Ersetzt ältere XHR-Implementierung
+- Implementiert Timeout-Handling, Fehlerbehandlung und Typsicherheit
+
+## ✅ Fehlerbehandlung
+- Zentrale Fehlerbehandlung in `/utils/error.ts` erstellt
+- Unterstützt verschiedene Fehlertypen (API, Netzwerk, Validierung, etc.)
+- Bietet einheitliche Fehleranzeige und -protokollierung
+
+## ✅ UI-Komponenten
+- `useChatInput`-Hook für Eingabefelder erstellt
+- `ChatInput`-Komponente vereinheitlicht die verschiedenen Nachrichteneingabefelder
+- `MessageRenderer`-Komponente für einheitliche Nachrichtenanzeige erstellt
+
+## ✅ Services
+- `modelService.ts` zentralisiert die Modell-Logik
+- Implementiert Caching, Fallback-Modelle und Validierung
+
+## ⏳ Noch ausstehend
+- Umstellung redundanter Modell-Code auf den neuen `modelService`
+- Konsolidierung der Konversationslogik
+- Standardisierung aller Komponenten auf NativeWind
+- Erstellen weiterer gemeinsamer React Hooks
+
+## Verbesserungen
+1. **Einfachere Codeorganisation**: zentrale Typen, weniger doppelter Code
+2. **Verbesserte Fehlerbehandlung**: konsistente Fehlermeldungen
+3. **Reduzierte Redundanz**: vereinheitlichte UI-Komponenten
+4. **Bessere Wartbarkeit**: klare Trennung zwischen Datenzugriff und UI
\ No newline at end of file
diff --git a/chat/apps/mobile/app-env.d.ts b/chat/apps/mobile/app-env.d.ts
new file mode 100644
index 000000000..88dc403ea
--- /dev/null
+++ b/chat/apps/mobile/app-env.d.ts
@@ -0,0 +1,2 @@
+// @ts-ignore
+///
diff --git a/chat/apps/mobile/app.json b/chat/apps/mobile/app.json
new file mode 100644
index 000000000..232ce735c
--- /dev/null
+++ b/chat/apps/mobile/app.json
@@ -0,0 +1,56 @@
+{
+ "expo": {
+ "name": "chat",
+ "slug": "chat",
+ "version": "1.0.0",
+ "scheme": "chat",
+ "web": {
+ "bundler": "metro",
+ "output": "server",
+ "favicon": "./assets/favicon.png"
+ },
+ "plugins": [
+ "expo-router",
+ [
+ "expo-dev-launcher",
+ {
+ "launchMode": "most-recent"
+ }
+ ]
+ ],
+ "experiments": {
+ "typedRoutes": true,
+ "tsconfigPaths": true
+ },
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "userInterfaceStyle": "light",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true,
+ "bundleIdentifier": "com.tilljs.chat"
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#ffffff"
+ },
+ "package": "com.tilljs.chat"
+ },
+ "extra": {
+ "router": {
+ "origin": false
+ },
+ "eas": {
+ "projectId": "67f22a8b-3cae-487d-af1f-55bdaca50e81"
+ }
+ }
+ }
+}
diff --git a/chat/apps/mobile/app/(drawer)/_layout.tsx b/chat/apps/mobile/app/(drawer)/_layout.tsx
new file mode 100644
index 000000000..80add684b
--- /dev/null
+++ b/chat/apps/mobile/app/(drawer)/_layout.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
+import { Drawer } from 'expo-router/drawer';
+import { Ionicons } from '@expo/vector-icons';
+import { useAppTheme } from '../../theme/ThemeProvider';
+
+export default function DrawerLayout() {
+ const { isDarkMode } = useAppTheme();
+
+ // Anpassen des Drawer-Stils basierend auf dem Farbschema
+ const drawerStyles = {
+ backgroundColor: isDarkMode ? '#1C1C1E' : '#FFFFFF',
+ contentOptions: {
+ activeTintColor: '#0A84FF',
+ inactiveTintColor: isDarkMode ? '#FFFFFF' : '#000000',
+ activeBackgroundColor: isDarkMode ? '#2C2C2E' : '#E5E5EA',
+ },
+ };
+
+ return (
+
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/chat/apps/mobile/app/+html.tsx b/chat/apps/mobile/app/+html.tsx
new file mode 100644
index 000000000..2fe284848
--- /dev/null
+++ b/chat/apps/mobile/app/+html.tsx
@@ -0,0 +1,46 @@
+import { ScrollViewStyleReset } from 'expo-router/html';
+
+// This file is web-only and used to configure the root HTML for every
+// web page during static rendering.
+// The contents of this function only run in Node.js environments and
+// do not have access to the DOM or browser APIs.
+export default function Root({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+ {/*
+ This viewport disables scaling which makes the mobile website act more like a native app.
+ However this does reduce built-in accessibility. If you want to enable scaling, use this instead:
+
+ */}
+
+ {/*
+ Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
+ However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
+ */}
+
+
+ {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
+
+ {/* Add any additional elements that you want globally available on web... */}
+
+ {children}
+
+ );
+}
+
+const responsiveBackground = `
+body {
+ background-color: #fff;
+}
+@media (prefers-color-scheme: dark) {
+ body {
+ background-color: #000;
+ }
+}`;
diff --git a/chat/apps/mobile/app/+not-found.tsx b/chat/apps/mobile/app/+not-found.tsx
new file mode 100644
index 000000000..241be1517
--- /dev/null
+++ b/chat/apps/mobile/app/+not-found.tsx
@@ -0,0 +1,24 @@
+import { Link, Stack } from 'expo-router';
+import { Text } from 'react-native';
+
+import { Container } from '~/components/Container';
+
+export default function NotFoundScreen() {
+ return (
+ <>
+
+
+ This screen doesn't exist.
+
+ Go to home screen!
+
+
+ >
+ );
+}
+
+const styles = {
+ title: `text-xl font-bold`,
+ link: `mt-4 pt-4`,
+ linkText: `text-base text-[#2e78b7]`,
+};
diff --git a/chat/apps/mobile/app/_layout.tsx b/chat/apps/mobile/app/_layout.tsx
new file mode 100644
index 000000000..114cf521d
--- /dev/null
+++ b/chat/apps/mobile/app/_layout.tsx
@@ -0,0 +1,72 @@
+import '../global.css';
+
+import { Stack, useRouter, useSegments } from 'expo-router';
+import { GestureHandlerRootView } from 'react-native-gesture-handler';
+import { ThemeProvider as NavigationThemeProvider } from '@react-navigation/native';
+import { useAppTheme } from '../theme/ThemeProvider';
+import { ThemeProvider } from '../theme/ThemeProvider';
+import { AuthProvider, useAuth } from '../context/AuthProvider';
+import { useEffect } from 'react';
+
+export const unstable_settings = {
+ // Ensure that reloading on `/modal` keeps a back button present.
+ initialRouteName: '(drawer)',
+};
+
+function Layout() {
+ const { theme } = useAppTheme();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// Authentifizierungsprüfung und Umleitung
+function AuthGuard({ children }: { children: React.ReactNode }) {
+ const { user, loading } = useAuth();
+ const segments = useSegments();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (loading) return;
+
+ const inAuthGroup = segments[0] === 'auth';
+
+ if (!user && !inAuthGroup) {
+ // Wenn kein Benutzer angemeldet ist und nicht auf einer Auth-Seite, zur Login-Seite umleiten
+ router.replace('/auth/login');
+ } else if (user && inAuthGroup) {
+ // Wenn ein Benutzer angemeldet ist und auf einer Auth-Seite, zur Hauptseite umleiten
+ router.replace('/');
+ }
+ }, [user, loading, segments]);
+
+ return <>{children}>;
+}
+
+export default function RootLayout() {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/chat/apps/mobile/app/api/models+api.ts b/chat/apps/mobile/app/api/models+api.ts
new file mode 100644
index 000000000..ffd01c640
--- /dev/null
+++ b/chat/apps/mobile/app/api/models+api.ts
@@ -0,0 +1,148 @@
+import { supabase } from '../../utils/supabase';
+
+// Definiere den Typ für ein Modell
+export type Model = {
+ id: string;
+ name: string;
+ description: string;
+ parameters?: Record;
+ created_at?: string;
+ updated_at?: string;
+};
+
+// Fallback-Modelle, falls keine aus der Datenbank geladen werden können
+const FALLBACK_MODELS: Model[] = [
+ {
+ id: '550e8400-e29b-41d4-a716-446655440000',
+ name: 'GPT-O3-Mini',
+ description: 'Azure OpenAI O3-Mini: Effizientes Modell für schnelle Antworten.',
+ parameters: {
+ temperature: 0.7,
+ max_tokens: 800,
+ provider: 'azure',
+ deployment: 'gpt-o3-mini-se',
+ endpoint: 'https://memoroseopenai.openai.azure.com',
+ api_version: '2024-12-01-preview'
+ }
+ },
+ {
+ id: '550e8400-e29b-41d4-a716-446655440004',
+ name: 'GPT-4o-Mini',
+ description: 'Azure OpenAI GPT-4o-Mini: Kompaktes, leistungsstarkes KI-Modell.',
+ parameters: {
+ temperature: 0.7,
+ max_tokens: 1000,
+ provider: 'azure',
+ deployment: 'gpt-4o-mini-se',
+ endpoint: 'https://memoroseopenai.openai.azure.com',
+ api_version: '2024-12-01-preview'
+ }
+ },
+ {
+ id: '550e8400-e29b-41d4-a716-446655440005',
+ name: 'GPT-4o',
+ description: 'Azure OpenAI GPT-4o: Das fortschrittlichste multimodale KI-Modell.',
+ parameters: {
+ temperature: 0.7,
+ max_tokens: 1200,
+ provider: 'azure',
+ deployment: 'gpt-4o-se',
+ endpoint: 'https://memoroseopenai.openai.azure.com',
+ api_version: '2024-12-01-preview'
+ }
+ }
+];
+
+// GET-Handler für Modelle
+export async function GET(request: Request) {
+ try {
+ // Versuche, Modelle aus der Supabase-Datenbank zu laden
+ let models: Model[] = FALLBACK_MODELS;
+
+ // Wenn Supabase konfiguriert ist, versuche die Modelle von dort zu laden
+ try {
+ if (supabase) {
+ const { data, error } = await supabase
+ .from('models')
+ .select('*');
+ // Entfernt: .order('created_at', { ascending: false })
+
+ if (error) {
+ console.error('Fehler beim Laden der Modelle aus Supabase:', error);
+ } else if (data && data.length > 0) {
+ models = data as Model[];
+ }
+ }
+ } catch (e) {
+ console.error('Fehler bei der Supabase-Verbindung:', e);
+ // Fallback zu den vordefinierten Modellen
+ }
+
+ return Response.json(models);
+ } catch (error) {
+ console.error('Fehler beim Verarbeiten der Anfrage:', error);
+ return new Response(JSON.stringify({ error: 'Interner Serverfehler' }), {
+ status: 500,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ }
+}
+
+// POST-Handler zum Erstellen eines neuen Modells
+export async function POST(request: Request) {
+ try {
+ const body = await request.json();
+
+ // Validiere die Eingabedaten
+ if (!body.name || !body.description) {
+ return new Response(JSON.stringify({ error: 'Name und Beschreibung sind erforderlich' }), {
+ status: 400,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ }
+
+ // Erstelle ein neues Modell in der Datenbank
+ if (supabase) {
+ const { data, error } = await supabase
+ .from('models')
+ .insert([{
+ name: body.name,
+ description: body.description,
+ parameters: body.parameters || {},
+ }])
+ .select();
+
+ if (error) {
+ console.error('Fehler beim Erstellen des Modells:', error);
+ return new Response(JSON.stringify({ error: 'Fehler beim Erstellen des Modells' }), {
+ status: 500,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ }
+
+ return Response.json(data[0]);
+ } else {
+ // Wenn Supabase nicht verfügbar ist, gib einen Fehler zurück
+ return new Response(JSON.stringify({ error: 'Datenbank nicht verfügbar' }), {
+ status: 503,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ }
+ } catch (error) {
+ console.error('Fehler beim Verarbeiten der Anfrage:', error);
+ return new Response(JSON.stringify({ error: 'Interner Serverfehler' }), {
+ status: 500,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ }
+}
diff --git a/chat/apps/mobile/app/api/usage+api.ts b/chat/apps/mobile/app/api/usage+api.ts
new file mode 100644
index 000000000..759fb2249
--- /dev/null
+++ b/chat/apps/mobile/app/api/usage+api.ts
@@ -0,0 +1,137 @@
+import { supabase } from '../../utils/supabase';
+
+// Typ für die Token-Nutzung pro Modell
+export type ModelUsage = {
+ model_id: string;
+ model_name: string;
+ total_prompt_tokens: number;
+ total_completion_tokens: number;
+ total_tokens: number;
+ total_cost: number;
+};
+
+// Typ für die Token-Nutzung nach Zeitraum
+export type UsageByPeriod = {
+ time_period: string;
+ total_tokens: number;
+ total_cost: number;
+};
+
+// Typ für die Token-Nutzung einer Konversation
+export type ConversationUsage = {
+ message_id: string;
+ created_at: string;
+ prompt_tokens: number;
+ completion_tokens: number;
+ total_tokens: number;
+ estimated_cost: number;
+};
+
+// Handler für GET /api/usage
+export async function GET(request: Request) {
+ try {
+ const url = new URL(request.url);
+ const userId = url.searchParams.get('userId');
+ const period = url.searchParams.get('period') || 'month';
+
+ if (!userId) {
+ return new Response(JSON.stringify({ error: 'User ID ist erforderlich' }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ // Lade die Tokennutzung nach Modell
+ const { data: modelUsage, error: modelError } = await supabase
+ .rpc('get_user_model_usage', { user_id: userId });
+
+ if (modelError) {
+ console.error('Fehler beim Laden der Modellnutzung:', modelError);
+ return new Response(JSON.stringify({ error: 'Fehler beim Laden der Modellnutzung' }), {
+ status: 500,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ // Lade die Tokennutzung nach Zeitraum
+ const { data: periodUsage, error: periodError } = await supabase
+ .rpc('get_user_usage_by_period', {
+ user_id: userId,
+ period: period
+ });
+
+ if (periodError) {
+ console.error('Fehler beim Laden der Zeitraumnutzung:', periodError);
+ return new Response(JSON.stringify({ error: 'Fehler beim Laden der Zeitraumnutzung' }), {
+ status: 500,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ // Berechne Gesamtkosten und Token
+ const totalCost = (modelUsage as ModelUsage[]).reduce((sum, model) => sum + model.total_cost, 0);
+ const totalTokens = (modelUsage as ModelUsage[]).reduce((sum, model) => sum + model.total_tokens, 0);
+
+ return Response.json({
+ modelUsage,
+ periodUsage,
+ summary: {
+ totalCost,
+ totalTokens
+ }
+ });
+ } catch (error) {
+ console.error('Fehler beim Verarbeiten der Anfrage:', error);
+ return new Response(JSON.stringify({ error: 'Interner Serverfehler' }), {
+ status: 500,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+}
+
+// Handler für GET /api/usage/conversation
+export async function GET_conversation(request: Request) {
+ try {
+ const url = new URL(request.url);
+ const conversationId = url.searchParams.get('conversationId');
+
+ if (!conversationId) {
+ return new Response(JSON.stringify({ error: 'Conversation ID ist erforderlich' }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ // Lade die Tokennutzung für die Konversation
+ const { data: conversationUsage, error } = await supabase
+ .rpc('get_conversation_usage', { conversation_id: conversationId });
+
+ if (error) {
+ console.error('Fehler beim Laden der Konversationsnutzung:', error);
+ return new Response(JSON.stringify({ error: 'Fehler beim Laden der Konversationsnutzung' }), {
+ status: 500,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ // Berechne Gesamtkosten und Token für diese Konversation
+ const usage = conversationUsage as ConversationUsage[];
+ const totalCost = usage.reduce((sum, item) => sum + item.estimated_cost, 0);
+ const totalTokens = usage.reduce((sum, item) => sum + item.total_tokens, 0);
+
+ return Response.json({
+ conversationUsage,
+ summary: {
+ totalCost,
+ totalTokens,
+ messageCount: usage.length
+ }
+ });
+ } catch (error) {
+ console.error('Fehler beim Verarbeiten der Anfrage:', error);
+ return new Response(JSON.stringify({ error: 'Interner Serverfehler' }), {
+ status: 500,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+}
\ No newline at end of file
diff --git a/chat/apps/mobile/app/archive.tsx b/chat/apps/mobile/app/archive.tsx
new file mode 100644
index 000000000..6699142c3
--- /dev/null
+++ b/chat/apps/mobile/app/archive.tsx
@@ -0,0 +1,507 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ FlatList,
+ TouchableOpacity,
+ SafeAreaView,
+ Alert,
+ ActivityIndicator
+} from 'react-native';
+import { useTheme, useFocusEffect } from '@react-navigation/native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { useAuth } from '../context/AuthProvider';
+import { useAppTheme } from '../theme/ThemeProvider';
+import CustomDrawer from '../components/CustomDrawer';
+import {
+ getArchivedConversations,
+ getMessages,
+ deleteConversation,
+ unarchiveConversation
+} from '../services/conversation';
+import { supabase } from '../utils/supabase';
+
+// Typendefinitionen für Konversationen
+type ConversationItem = {
+ id: string;
+ modelName: string;
+ title: string;
+ lastMessage: string;
+ timestamp: Date;
+ mode: 'frei' | 'geführt' | 'vorlage';
+};
+
+// Hilfsfunktion zur Formatierung des Datums
+const formatDate = (date: Date) => {
+ const day = date.getDate().toString().padStart(2, '0');
+ const month = new Intl.DateTimeFormat('de-DE', { month: 'short' }).format(date);
+ const hours = date.getHours().toString().padStart(2, '0');
+ const minutes = date.getMinutes().toString().padStart(2, '0');
+ return `${day}. ${month}, ${hours}:${minutes}`;
+};
+
+export default function ArchiveScreen() {
+ const { colors } = useTheme();
+ const router = useRouter();
+ const { user } = useAuth();
+ const [conversations, setConversations] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const { isDarkMode } = useAppTheme();
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false);
+
+ // Eine Funktion, die Konversationen lädt und wiederverwendet werden kann
+ const loadConversations = async () => {
+ if (!user) return;
+
+ setIsLoading(true);
+ try {
+ console.log("Lade archivierte Konversationen für User:", user.id);
+ // Lade alle archivierten Konversationen des Benutzers
+ const userConversations = await getArchivedConversations(user.id);
+ console.log(`${userConversations.length} archivierte Konversationen geladen`, new Date().toLocaleTimeString());
+
+ // Lade für jede Konversation die letzte Nachricht und das Modell
+ const conversationItems: ConversationItem[] = [];
+
+ for (const conv of userConversations) {
+ try {
+ // Lade die Nachrichten der Konversation
+ const messages = await getMessages(conv.id);
+ // Lade das Modell aus der Datenbank
+ const { data: modelData } = await supabase
+ .from('models')
+ .select('name')
+ .eq('id', conv.model_id)
+ .single();
+
+ // Finde die letzte Nachricht (die nicht vom System ist)
+ const lastMessage = messages
+ .filter(msg => msg.sender !== 'system')
+ .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
+
+ if (lastMessage) {
+ conversationItems.push({
+ id: conv.id,
+ modelName: modelData?.name || 'Unbekanntes Modell',
+ title: conv.title || 'Unbenannte Konversation',
+ lastMessage: lastMessage.message_text,
+ timestamp: new Date(conv.updated_at),
+ mode: conv.conversation_mode === 'free' ? 'frei' :
+ conv.conversation_mode === 'guided' ? 'geführt' : 'vorlage'
+ });
+ }
+ } catch (error) {
+ console.error(`Fehler beim Laden der Details für Konversation ${conv.id}:`, error);
+ }
+ }
+
+ setConversations(conversationItems);
+ } catch (error) {
+ console.error('Fehler beim Laden der Konversationen:', error);
+ Alert.alert('Fehler', 'Die Konversationen konnten nicht geladen werden.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Lade die Konversationen beim ersten Rendern und wenn sich der User ändert
+ useEffect(() => {
+ loadConversations();
+ }, [user]);
+
+ // Lade Konversationen erneut, wenn der Screen fokussiert wird
+ useFocusEffect(
+ useCallback(() => {
+ if (user) loadConversations();
+ return () => {};
+ }, [user])
+ );
+
+ const handleConversationPress = (id: string) => {
+ // Navigiere zum Konversations-Screen mit der ID
+ router.push(`/conversation/${id}`);
+ };
+
+ // Löschen einer Konversation
+ const handleDeleteConversation = (id: string) => {
+ Alert.alert(
+ "Konversation löschen",
+ "Möchtest du diese Konversation wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
+ [
+ {
+ text: "Abbrechen",
+ style: "cancel"
+ },
+ {
+ text: "Löschen",
+ style: "destructive",
+ onPress: async () => {
+ try {
+ const success = await deleteConversation(id);
+ if (success) {
+ // Aus der lokalen Liste entfernen
+ setConversations(prev => prev.filter(conv => conv.id !== id));
+ Alert.alert("Erfolg", "Die Konversation wurde gelöscht.");
+ } else {
+ Alert.alert("Fehler", "Die Konversation konnte nicht gelöscht werden.");
+ }
+ } catch (error) {
+ console.error('Fehler beim Löschen der Konversation:', error);
+ Alert.alert("Fehler", "Die Konversation konnte nicht gelöscht werden.");
+ }
+ }
+ }
+ ]
+ );
+ };
+
+ // Wiederherstellen einer archivierten Konversation
+ const handleUnarchiveConversation = async (id: string) => {
+ try {
+ const success = await unarchiveConversation(id);
+ if (success) {
+ // Aus der lokalen Liste entfernen
+ setConversations(prev => prev.filter(conv => conv.id !== id));
+ Alert.alert("Erfolg", "Die Konversation wurde wiederhergestellt.");
+ } else {
+ Alert.alert("Fehler", "Die Konversation konnte nicht wiederhergestellt werden.");
+ }
+ } catch (error) {
+ console.error('Fehler beim Wiederherstellen der Konversation:', error);
+ Alert.alert("Fehler", "Die Konversation konnte nicht wiederhergestellt werden.");
+ }
+ };
+
+ // Zustandsverwaltung für die Optionsmenüs der Konversationselemente
+ const [expandedConversationId, setExpandedConversationId] = useState(null);
+
+ // Toggle-Funktion für das Optionsmenü
+ const toggleOptionsMenu = (id: string) => {
+ setExpandedConversationId(expandedConversationId === id ? null : id);
+ };
+
+ const renderConversationItem = ({ item }: { item: ConversationItem }) => {
+ const showOptions = expandedConversationId === item.id;
+
+ return (
+
+ handleConversationPress(item.id)}
+ onLongPress={() => toggleOptionsMenu(item.id)}
+ >
+
+
+
+
+
+ {item.title}
+
+
+
+ {formatDate(item.timestamp)}
+
+
+
+
+ {item.modelName}
+
+
+
+
+ {item.lastMessage}
+
+
+
+
+ {item.mode === 'frei' ? 'Freier Modus' :
+ item.mode === 'geführt' ? 'Geführter Modus' : 'Vorlagen-Modus'}
+
+
+
+
+ toggleOptionsMenu(item.id)}>
+
+
+
+
+ {showOptions && (
+
+ handleUnarchiveConversation(item.id)}
+ >
+
+ Wiederherstellen
+
+
+ handleDeleteConversation(item.id)}
+ >
+
+ Löschen
+
+
+ )}
+
+ );
+ };
+
+ return (
+
+
+ {/* Permanenter Drawer links */}
+ {isDrawerOpen && (
+
+ setIsDrawerOpen(false)}
+ />
+
+ )}
+
+ {/* Hauptinhalt */}
+
+
+
+ setIsDrawerOpen(!isDrawerOpen)}
+ >
+
+
+
+
+ router.back()}
+ >
+
+
+
+ Archiv
+
+
+
+ {/* Konversationsliste */}
+
+ {isLoading ? (
+
+
+
+ Konversationen werden geladen...
+
+
+ ) : conversations.length > 0 ? (
+ item.id}
+ renderItem={renderConversationItem}
+ contentContainerStyle={styles.listContent}
+ />
+ ) : (
+
+
+
+ Keine archivierten Konversationen
+
+
+ Archivierte Gespräche erscheinen hier
+
+
+ )}
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ mainLayout: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+ mainContainer: {
+ flex: 1,
+ alignItems: 'center',
+ },
+ drawerContainer: {
+ width: 260,
+ height: '100%',
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ bottom: 0,
+ zIndex: 10,
+ },
+ contentContainer: {
+ flex: 1,
+ maxWidth: 1200,
+ width: '100%',
+ },
+ headerContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ width: '100%',
+ paddingHorizontal: 16,
+ paddingTop: 12,
+ paddingBottom: 8,
+ },
+ menuButton: {
+ padding: 12,
+ marginRight: 0,
+ zIndex: 5,
+ },
+ headerContentContainer: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ backButton: {
+ padding: 8,
+ marginRight: 8,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ },
+ listContainer: {
+ flex: 1,
+ width: '100%',
+ maxWidth: 800,
+ alignSelf: 'center',
+ },
+ listContent: {
+ paddingHorizontal: 16,
+ paddingBottom: 120,
+ width: '100%',
+ maxWidth: 800,
+ alignSelf: 'center',
+ },
+ conversationItemWrapper: {
+ borderRadius: 12,
+ marginTop: 12,
+ overflow: 'hidden',
+ },
+ conversationItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 16,
+ },
+ conversationContent: {
+ flex: 1,
+ },
+ optionsContainer: {
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ paddingHorizontal: 16,
+ paddingBottom: 12,
+ paddingTop: 4,
+ borderTopWidth: StyleSheet.hairlineWidth,
+ borderTopColor: 'rgba(0,0,0,0.1)',
+ },
+ optionButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ marginLeft: 12,
+ },
+ optionText: {
+ fontSize: 14,
+ marginLeft: 6,
+ fontWeight: '500',
+ },
+ conversationHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 6,
+ },
+ titleRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ },
+ titleIcon: {
+ marginRight: 8,
+ },
+ timestamp: {
+ fontSize: 12,
+ },
+ modelContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 6,
+ },
+ modelName: {
+ fontSize: 12,
+ fontWeight: '400',
+ },
+ lastMessage: {
+ fontSize: 14,
+ marginBottom: 6,
+ },
+ modeContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ modeText: {
+ fontSize: 12,
+ },
+ // Container für den Ladezustand
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 32,
+ marginTop: 40,
+ },
+ loadingText: {
+ fontSize: 16,
+ marginTop: 16,
+ textAlign: 'center',
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 32,
+ marginTop: 40,
+ },
+ emptyText: {
+ fontSize: 18,
+ fontWeight: '600',
+ marginTop: 16,
+ textAlign: 'center',
+ },
+ emptySubtext: {
+ fontSize: 14,
+ marginTop: 8,
+ textAlign: 'center',
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/app/auth/_layout.tsx b/chat/apps/mobile/app/auth/_layout.tsx
new file mode 100644
index 000000000..8a21f5148
--- /dev/null
+++ b/chat/apps/mobile/app/auth/_layout.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { Stack } from 'expo-router';
+
+export default function AuthLayout() {
+ return (
+
+ );
+}
diff --git a/chat/apps/mobile/app/auth/login.tsx b/chat/apps/mobile/app/auth/login.tsx
new file mode 100644
index 000000000..cfcfeda3f
--- /dev/null
+++ b/chat/apps/mobile/app/auth/login.tsx
@@ -0,0 +1,295 @@
+import React, { useState } from 'react';
+import { View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator, Alert } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { useRouter, Link } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { useAuth } from '../../context/AuthProvider';
+import { supabase } from '../../utils/supabase';
+import { useAppTheme } from '../../theme/ThemeProvider';
+
+export default function LoginScreen() {
+ const { colors } = useTheme();
+ const { isDarkMode } = useAppTheme();
+ const router = useRouter();
+ const { signIn } = useAuth();
+
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const [isMagicLinkSent, setIsMagicLinkSent] = useState(false);
+
+ const handleLogin = async () => {
+ if (!email || !password) {
+ Alert.alert('Fehler', 'Bitte gib deine E-Mail-Adresse und dein Passwort ein.');
+ return;
+ }
+
+ try {
+ setLoading(true);
+ const { error } = await signIn(email, password);
+
+ if (error) {
+ console.log('Anmeldung mit Passwort fehlgeschlagen, versuche direkte Anmeldung...');
+ // Wenn die normale Anmeldung fehlschlägt, versuche eine direkte Anmeldung
+ const { error: directError } = await supabase.auth.signInWithPassword({
+ email,
+ password,
+ });
+
+ if (directError) {
+ Alert.alert('Anmeldung fehlgeschlagen', directError.message);
+ } else {
+ router.replace('/');
+ }
+ } else {
+ // Erfolgreich angemeldet, navigiere zur Hauptseite
+ router.replace('/');
+ }
+ } catch (error) {
+ console.error('Fehler bei der Anmeldung:', error);
+ Alert.alert('Fehler', 'Bei der Anmeldung ist ein Fehler aufgetreten. Bitte versuche es später erneut.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleMagicLink = async () => {
+ if (!email) {
+ Alert.alert('Fehler', 'Bitte gib deine E-Mail-Adresse ein.');
+ return;
+ }
+
+ try {
+ setLoading(true);
+
+ const { error } = await supabase.auth.signInWithOtp({
+ email,
+ options: {
+ emailRedirectTo: 'exp://localhost:8081/',
+ },
+ });
+
+ if (error) {
+ Alert.alert('Fehler', error.message);
+ } else {
+ setIsMagicLinkSent(true);
+ Alert.alert(
+ 'Magic Link gesendet',
+ 'Wir haben dir einen Magic Link an deine E-Mail-Adresse gesendet. Bitte öffne den Link, um dich anzumelden.'
+ );
+ }
+ } catch (error) {
+ console.error('Fehler beim Senden des Magic Links:', error);
+ Alert.alert('Fehler', 'Beim Senden des Magic Links ist ein Fehler aufgetreten. Bitte versuche es später erneut.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ Willkommen zurück
+
+ Melde dich an, um deine Konversationen fortzusetzen
+
+
+
+
+
+ E-Mail
+
+
+
+
+
+
+
+ Passwort
+
+
+
+ setShowPassword(!showPassword)}>
+
+
+
+
+
+ router.push('/auth/reset-password')}
+ >
+
+ Passwort vergessen?
+
+
+
+
+ {loading ? (
+
+ ) : (
+ Anmelden
+ )}
+
+
+
+ {loading ? (
+
+ ) : (
+
+ {isMagicLinkSent ? 'Magic Link gesendet' : 'Mit Magic Link anmelden'}
+
+ )}
+
+
+
+
+ Noch kein Konto?
+
+
+
+
+ Registrieren
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 24,
+ },
+ header: {
+ marginTop: 40,
+ marginBottom: 40,
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+ subtitle: {
+ fontSize: 16,
+ },
+ form: {
+ width: '100%',
+ },
+ inputContainer: {
+ marginBottom: 20,
+ },
+ label: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginBottom: 8,
+ },
+ inputWrapper: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderWidth: 1,
+ borderRadius: 12,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+ input: {
+ flex: 1,
+ fontSize: 16,
+ marginLeft: 12,
+ },
+ forgotPassword: {
+ alignSelf: 'flex-end',
+ marginBottom: 24,
+ },
+ forgotPasswordText: {
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ loginButton: {
+ height: 56,
+ borderRadius: 12,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ magicLinkButton: {
+ height: 56,
+ borderRadius: 12,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: 24,
+ },
+ magicLinkButtonText: {
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ loginButtonText: {
+ color: '#FFFFFF',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ signupContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ signupText: {
+ fontSize: 14,
+ marginRight: 4,
+ },
+ signupLink: {
+ fontSize: 14,
+ fontWeight: '600',
+ },
+});
diff --git a/chat/apps/mobile/app/auth/register.tsx b/chat/apps/mobile/app/auth/register.tsx
new file mode 100644
index 000000000..2f6046e26
--- /dev/null
+++ b/chat/apps/mobile/app/auth/register.tsx
@@ -0,0 +1,244 @@
+import React, { useState } from 'react';
+import { View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator, Alert } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { useRouter, Link } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { useAuth } from '../../context/AuthProvider';
+import { useAppTheme } from '../../theme/ThemeProvider';
+
+export default function RegisterScreen() {
+ const { colors } = useTheme();
+ const { isDarkMode } = useAppTheme();
+ const router = useRouter();
+ const { signUp } = useAuth();
+
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+
+ const handleRegister = async () => {
+ if (!email || !password || !confirmPassword) {
+ Alert.alert('Fehler', 'Bitte fülle alle Felder aus.');
+ return;
+ }
+
+ if (password !== confirmPassword) {
+ Alert.alert('Fehler', 'Die Passwörter stimmen nicht überein.');
+ return;
+ }
+
+ if (password.length < 6) {
+ Alert.alert('Fehler', 'Das Passwort muss mindestens 6 Zeichen lang sein.');
+ return;
+ }
+
+ try {
+ setLoading(true);
+ const { data, error } = await signUp(email, password);
+
+ if (error) {
+ Alert.alert('Registrierung fehlgeschlagen', error.message);
+ } else if (data?.user) {
+ Alert.alert(
+ 'Registrierung erfolgreich',
+ 'Dein Konto wurde erfolgreich erstellt. Du wirst jetzt angemeldet.',
+ [
+ {
+ text: 'OK',
+ onPress: () => router.replace('/')
+ }
+ ]
+ );
+ }
+ } catch (error) {
+ console.error('Fehler bei der Registrierung:', error);
+ Alert.alert('Fehler', 'Bei der Registrierung ist ein Fehler aufgetreten. Bitte versuche es später erneut.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ Konto erstellen
+
+ Erstelle ein Konto, um mit KI-Modellen zu chatten
+
+
+
+
+
+ E-Mail
+
+
+
+
+
+
+
+ Passwort
+
+
+
+ setShowPassword(!showPassword)}>
+
+
+
+
+
+
+ Passwort bestätigen
+
+
+
+
+
+
+
+ {loading ? (
+
+ ) : (
+ Registrieren
+ )}
+
+
+
+
+ Bereits ein Konto?
+
+
+
+
+ Anmelden
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 24,
+ },
+ header: {
+ marginTop: 40,
+ marginBottom: 40,
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+ subtitle: {
+ fontSize: 16,
+ },
+ form: {
+ width: '100%',
+ },
+ inputContainer: {
+ marginBottom: 20,
+ },
+ label: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginBottom: 8,
+ },
+ inputWrapper: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderWidth: 1,
+ borderRadius: 12,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+ input: {
+ flex: 1,
+ fontSize: 16,
+ marginLeft: 12,
+ },
+ registerButton: {
+ height: 56,
+ borderRadius: 12,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginTop: 12,
+ marginBottom: 24,
+ },
+ registerButtonText: {
+ color: '#FFFFFF',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ loginContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ loginText: {
+ fontSize: 14,
+ marginRight: 4,
+ },
+ loginLink: {
+ fontSize: 14,
+ fontWeight: '600',
+ },
+});
diff --git a/chat/apps/mobile/app/auth/reset-password.tsx b/chat/apps/mobile/app/auth/reset-password.tsx
new file mode 100644
index 000000000..c831167b6
--- /dev/null
+++ b/chat/apps/mobile/app/auth/reset-password.tsx
@@ -0,0 +1,172 @@
+import React, { useState } from 'react';
+import { View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator, Alert } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { useAuth } from '../../context/AuthProvider';
+import { useAppTheme } from '../../theme/ThemeProvider';
+
+export default function ResetPasswordScreen() {
+ const { colors } = useTheme();
+ const { isDarkMode } = useAppTheme();
+ const router = useRouter();
+ const { resetPassword } = useAuth();
+
+ const [email, setEmail] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleResetPassword = async () => {
+ if (!email) {
+ Alert.alert('Fehler', 'Bitte gib deine E-Mail-Adresse ein.');
+ return;
+ }
+
+ try {
+ setLoading(true);
+ const { error } = await resetPassword(email);
+
+ if (error) {
+ Alert.alert('Fehler', error.message);
+ } else {
+ Alert.alert(
+ 'E-Mail gesendet',
+ 'Eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts wurde an deine E-Mail-Adresse gesendet.',
+ [
+ {
+ text: 'OK',
+ onPress: () => router.replace('/auth/login')
+ }
+ ]
+ );
+ }
+ } catch (error) {
+ console.error('Fehler beim Zurücksetzen des Passworts:', error);
+ Alert.alert('Fehler', 'Beim Zurücksetzen des Passworts ist ein Fehler aufgetreten. Bitte versuche es später erneut.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ Passwort zurücksetzen
+
+ Gib deine E-Mail-Adresse ein, um einen Link zum Zurücksetzen deines Passworts zu erhalten
+
+
+
+
+
+ E-Mail
+
+
+
+
+
+
+
+ {loading ? (
+
+ ) : (
+ Link senden
+ )}
+
+
+ router.back()}
+ >
+
+ Zurück zur Anmeldung
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 24,
+ },
+ header: {
+ marginTop: 40,
+ marginBottom: 40,
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+ subtitle: {
+ fontSize: 16,
+ },
+ form: {
+ width: '100%',
+ },
+ inputContainer: {
+ marginBottom: 24,
+ },
+ label: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginBottom: 8,
+ },
+ inputWrapper: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderWidth: 1,
+ borderRadius: 12,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+ input: {
+ flex: 1,
+ fontSize: 16,
+ marginLeft: 12,
+ },
+ resetButton: {
+ height: 56,
+ borderRadius: 12,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: 24,
+ },
+ resetButtonText: {
+ color: '#FFFFFF',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ backButton: {
+ alignItems: 'center',
+ padding: 12,
+ },
+ backButtonText: {
+ fontSize: 16,
+ fontWeight: '500',
+ },
+});
diff --git a/chat/apps/mobile/app/conversation/[id].tsx b/chat/apps/mobile/app/conversation/[id].tsx
new file mode 100644
index 000000000..db195609b
--- /dev/null
+++ b/chat/apps/mobile/app/conversation/[id].tsx
@@ -0,0 +1,1006 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { View, StyleSheet, KeyboardAvoidingView, Platform, Alert, TouchableOpacity, SafeAreaView, Text } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+
+import ChatHeader from '../../components/ChatHeader';
+import MessageList from '../../components/MessageList';
+import MessageInput, { MessageInputRef } from '../../components/MessageInput';
+import CustomDrawer from '../../components/CustomDrawer';
+import DocumentPanel from '../../components/DocumentPanel';
+import DocumentVersions from '../../components/DocumentVersions';
+
+// Import der Konversations- und OpenAI-Services
+import { createConversation, addMessage, getMessages, sendMessageAndGetResponse, Message as DbMessage } from '../../services/conversation';
+import { supabase } from '../../utils/supabase';
+import { Document, createDocument, createDocumentVersion, getLatestDocument, getAllDocumentVersions, hasDocument, deleteDocumentVersion } from '../../services/document';
+
+// Typdefinition für die Nachrichten in der UI
+type UIMessage = {
+ id: string;
+ text: string;
+ sender: 'user' | 'ai';
+ timestamp: Date;
+ isLoading?: boolean;
+};
+
+// Konvertiere Datenbank-Nachrichten in UI-Nachrichten
+function convertDbToUiMessages(dbMessages: DbMessage[]): UIMessage[] {
+ return dbMessages.map(msg => ({
+ id: msg.id,
+ text: msg.message_text,
+ sender: msg.sender === 'assistant' ? 'ai' : msg.sender as 'user',
+ timestamp: new Date(msg.created_at)
+ }));
+}
+
+export default function ConversationScreen() {
+ const { colors } = useTheme();
+ const router = useRouter();
+ // Hole Parameter aus URL und Query-String
+ const params = useLocalSearchParams();
+ const { id } = params;
+
+ // Drawer (Seitenmenü) Status
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false);
+
+ // Extrahiere Modell-ID und andere Parameter
+ const modelId = params.modelId || params.model_id;
+ const mode = params.mode;
+ const initialMessage = params.initialMessage || params.initial_message;
+ const templateId = params.templateId || params.template_id;
+ const documentMode = (params.documentMode === 'true') || false;
+
+ // Protokolliere alle Parameter für Debugging
+ console.log("URL-Parameter erhalten:", JSON.stringify(params, null, 2));
+
+ const conversationId = id as string;
+ const isNewConversation = conversationId === 'new';
+ const initialMsg = initialMessage as string | undefined;
+
+ // Protokolliere spezifische Parameter
+ console.log(`Verarbeite Konversation: id=${conversationId}, modelId=${modelId}, neu=${isNewConversation}, initialMsg=${initialMsg?.substring(0, 30)}`);
+
+ const [messages, setMessages] = useState([]);
+ const [actualConversationId, setActualConversationId] = useState(isNewConversation ? null : conversationId);
+ const [isLoading, setIsLoading] = useState(false);
+ const [modelName, setModelName] = useState('');
+ const [modelData, setModelData] = useState(null);
+ const [conversationMode, setConversationMode] = useState(mode as string || 'frei');
+ const [userId, setUserId] = useState(null);
+ const [conversationTitle, setConversationTitle] = useState(undefined);
+ const messageInputRef = useRef(null);
+
+ // Dokumentmodus Zustände
+ const [isDocumentMode, setIsDocumentMode] = useState(documentMode);
+ const [currentDocument, setCurrentDocument] = useState(null);
+ const [documentVersions, setDocumentVersions] = useState([]);
+ const [isDocumentLoading, setIsDocumentLoading] = useState(false);
+ const [isVersionsModalVisible, setIsVersionsModalVisible] = useState(false);
+
+ // Überprüfe den aktuellen Benutzer
+ useEffect(() => {
+ const checkUser = async () => {
+ const { data } = await supabase.auth.getUser();
+ if (data?.user) {
+ setUserId(data.user.id);
+ } else {
+ // In einer echten App würden wir hier zur Login-Seite weiterleiten
+ // Für jetzt verwenden wir eine Test-ID
+ setUserId('test-user-id');
+ }
+ };
+ checkUser();
+ }, []);
+
+ // Lade das Modell
+ useEffect(() => {
+ const fetchModelData = async () => {
+ try {
+ console.log(`Model-Daten laden für id=${modelId || 'unbekannt'}, conv=${conversationId}, neu=${isNewConversation}`);
+
+ // Wenn wir bereits eine modelId haben (aus der URL), laden wir dieses zuerst
+ if (modelId && modelId !== 'undefined') {
+ console.log("Lade Modell mit ID aus URL:", modelId);
+ const response = await fetch(`/api/models`);
+ const models = await response.json();
+ const model = models.find((m: any) => m.id === modelId);
+
+ if (model) {
+ console.log("★ Model-Daten aus URL-Parameter geladen:", model.name, "mit deployment:", model.parameters?.deployment);
+ setModelName(model.name);
+ setModelData(model);
+ return; // Beende die Funktion, da wir das Modell bereits gefunden haben
+ } else {
+ console.warn("Modell mit ID aus URL nicht gefunden:", modelId);
+ }
+ }
+
+ // Wenn kein URL-Modell gefunden wurde oder keins angegeben war,
+ // hole die Konversation, um die Model-ID zu bekommen
+ if (!isNewConversation && conversationId) {
+ console.log("Hole Modell-ID aus Konversation:", conversationId);
+ const { data: conversationData, error: conversationError } = await supabase
+ .from('conversations')
+ .select('model_id, title')
+ .eq('id', conversationId)
+ .single();
+
+ if (conversationData && conversationData.model_id) {
+ console.log("✓ Model-ID aus der Konversation geladen:", conversationData.model_id);
+ // Setze das modelId, wenn wir es aus der Konversation bekommen haben
+ const fetchedModelId = conversationData.model_id;
+
+ // Setze den Titel aus der Konversation
+ if (conversationData.title) {
+ console.log("✓ Titel aus der Konversation geladen:", conversationData.title);
+ setConversationTitle(conversationData.title);
+ }
+
+ // Hole jetzt das Modell mit der ID
+ const response = await fetch(`/api/models`);
+ const models = await response.json();
+ const model = models.find((m: any) => m.id === fetchedModelId);
+
+ if (model) {
+ console.log("✓ Model-Daten aus Konversation geladen:", model.name);
+ setModelName(model.name);
+ setModelData(model);
+ } else {
+ console.warn("Modell mit ID aus Konversation nicht gefunden:", fetchedModelId);
+ }
+ } else if (conversationError) {
+ console.error('Fehler beim Laden der Konversation:', conversationError);
+ }
+ }
+ } catch (error) {
+ console.error('Fehler beim Laden des Modells:', error);
+ }
+ };
+ fetchModelData();
+ }, [modelId, conversationId, isNewConversation]);
+
+ // Lade Nachrichten für eine bestehende Konversation
+ useEffect(() => {
+ const loadExistingConversation = async () => {
+ if (!isNewConversation && conversationId) {
+ try {
+ setIsLoading(true);
+ const dbMessages = await getMessages(conversationId);
+ if (dbMessages.length > 0) {
+ setMessages(convertDbToUiMessages(dbMessages));
+ }
+
+ // Prüfe, ob es eine bestehende Konversation mit Dokumentmodus ist
+ if (conversationId) {
+ const { data: convData, error: convError } = await supabase
+ .from('conversations')
+ .select('document_mode')
+ .eq('id', conversationId)
+ .single();
+
+ if (convData && convData.document_mode) {
+ setIsDocumentMode(true);
+ await loadDocumentData(conversationId);
+ }
+ }
+ } catch (error) {
+ console.error('Fehler beim Laden der Konversation:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ if (!isNewConversation) {
+ loadExistingConversation();
+ }
+ }, [conversationId, isNewConversation]);
+
+ // Funktion zum Laden der Dokumentdaten
+ const loadDocumentData = async (convId: string) => {
+ try {
+ console.log(`[loadDocumentData] Lade Dokumentdaten für Konversation ${convId}`);
+ setIsDocumentLoading(true);
+
+ // Längere Verzögerung zur Sicherstellung, dass Datenbanktransaktionen Zeit haben
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // Direkter Supabase-Aufruf mit Cache-Umgehung
+ console.log('Lade alle Dokumentversionen direkt...');
+
+ // Generiere einen zufälligen String, um Caching zu verhindern
+ const timestamp = new Date().getTime();
+ const randomString = Math.random().toString(36).substring(2, 8);
+ const noCache = `${timestamp}-${randomString}`;
+
+ const { data: freshVersions, error } = await supabase
+ .from('documents')
+ .select(`*,noCacheKey:conversation_id(id)`)
+ .eq('conversation_id', convId)
+ .order('version', { ascending: false })
+ .filter('noCacheKey.id', 'not.is', null)
+ .limit(100);
+
+ if (error) {
+ console.error('Fehler beim direkten Laden der Dokumentversionen:', error);
+ setDocumentVersions([]);
+ return;
+ }
+
+ // Entferne noCacheKey-Feld
+ const cleanVersions = freshVersions.map(v => {
+ const { noCacheKey, ...rest } = v;
+ return rest;
+ });
+
+ console.log(`${cleanVersions.length} Dokumentversionen direkt geladen`);
+ setDocumentVersions(cleanVersions);
+
+ // Wenn Versionen existieren, nehme die neueste
+ if (cleanVersions.length > 0) {
+ console.log('Setze neuestes Dokument aus Liste', cleanVersions[0]);
+ setCurrentDocument(cleanVersions[0]);
+ } else {
+ // Wenn keine Versionen existieren, setze alles zurück
+ console.log('Keine Dokumentversionen vorhanden, setze null');
+ setCurrentDocument(null);
+ }
+ } catch (error) {
+ console.error('Fehler beim Laden der Dokumentdaten:', error);
+ if (error instanceof Error) {
+ console.error('Details:', error.message);
+ }
+ // Bei Fehler alles zurücksetzen
+ setCurrentDocument(null);
+ setDocumentVersions([]);
+ } finally {
+ setIsDocumentLoading(false);
+ }
+ };
+
+ // Handler für das Speichern eines neuen Dokuments
+ const handleSaveDocument = async (content: string) => {
+ if (!actualConversationId) return;
+
+ try {
+ setIsDocumentLoading(true);
+
+ // Prüfen, ob bereits ein Dokument existiert
+ const hasExistingDoc = await hasDocument(actualConversationId);
+
+ let result: Document | null;
+
+ if (hasExistingDoc) {
+ // Neue Version erstellen
+ result = await createDocumentVersion(actualConversationId, content);
+ } else {
+ // Neues Dokument erstellen
+ result = await createDocument(actualConversationId, content);
+ }
+
+ if (result) {
+ // Aktualisiere die lokalen Zustände
+ setCurrentDocument(result);
+ await loadDocumentData(actualConversationId);
+
+ // Füge eine systemische Nachricht hinzu
+ const versionText = hasExistingDoc ? `Version ${result.version}` : 'erste Version';
+ await addMessage(
+ actualConversationId,
+ 'system',
+ `Dokument ${versionText} erstellt.`
+ );
+
+ // Lade die Nachrichten neu
+ const dbMessages = await getMessages(actualConversationId);
+ setMessages(convertDbToUiMessages(dbMessages));
+ }
+ } catch (error) {
+ console.error('Fehler beim Speichern des Dokuments:', error);
+ Alert.alert('Fehler', 'Das Dokument konnte nicht gespeichert werden.');
+ } finally {
+ setIsDocumentLoading(false);
+ }
+ };
+
+ // Handler für das Anzeigen der Versionen
+ const handleShowVersions = () => {
+ setIsVersionsModalVisible(true);
+ };
+
+ // Handler für die Auswahl einer Version
+ const handleSelectVersion = (document: Document) => {
+ setCurrentDocument(document);
+ setIsVersionsModalVisible(false);
+ };
+
+ // Handler für das Löschen einer Version
+ const handleDeleteVersion = async (document: Document) => {
+ if (!actualConversationId) {
+ console.error('Keine aktuelle Konversations-ID verfügbar');
+ return;
+ }
+
+ try {
+ console.log(`[handleDeleteVersion] Versuche Dokumentversion ${document.version} (ID: ${document.id}) zu löschen`);
+ setIsDocumentLoading(true);
+
+ // Debug-Informationen
+ console.log('Aktuelle Konversation:', actualConversationId);
+ console.log('Aktuelles Dokument:', currentDocument?.id);
+ console.log('Zu löschendes Dokument:', document.id);
+
+ // Sicherstellen, dass die zu löschende Version nicht aktuell angezeigt wird
+ const isCurrentlyDisplayed = currentDocument?.id === document.id;
+
+ // Direkter Zugriff auf Supabase
+ console.log('Versuche direkte Löschung mit Supabase via delete');
+
+ // Wir verwenden einen speziellen Trick, um sicherzustellen, dass die Löschung
+ // Zeit hat, vollständig durchgeführt zu werden, bevor wir weitermachen
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ try {
+ // Direktes Löschen über die normale DELETE-Methode
+ const { error } = await supabase
+ .from('documents')
+ .delete()
+ .eq('id', document.id);
+
+ if (error) {
+ console.error('Fehler beim direkten Löschen:', error);
+ throw error;
+ }
+
+ console.log('Dokument erfolgreich gelöscht');
+
+ // Warten, damit die Datenbank Zeit hat, sich zu aktualisieren
+ await new Promise(resolve => setTimeout(resolve, 800));
+ } catch (deleteError) {
+ console.error('Fehler beim Löschen:', deleteError);
+ throw deleteError;
+ }
+
+ const success = true; // Wenn wir bis hierher kommen, war es erfolgreich
+ console.log('Löschvorgang Ergebnis: Erfolgreich');
+
+ if (success) {
+ console.log(`Dokumentversion ${document.version} erfolgreich gelöscht`);
+
+ // Systemische Nachricht hinzufügen
+ const messageId = await addMessage(
+ actualConversationId,
+ 'system',
+ `Dokumentversion ${document.version} wurde gelöscht.`
+ );
+ console.log('System-Nachricht hinzugefügt:', messageId);
+
+ // Nachrichten neu laden
+ const dbMessages = await getMessages(actualConversationId);
+ setMessages(convertDbToUiMessages(dbMessages));
+
+ // Dokumentversionen neu laden mit forcierter Aktualisierung
+ // Zuerst kurz warten, damit die DB-Änderungen sich vollständig auswirken können
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ await loadDocumentData(actualConversationId);
+
+ // Wenn die gerade angezeigte Version gelöscht wurde, zur neuesten wechseln
+ if (isCurrentlyDisplayed) {
+ console.log('Aktuell angezeigte Version wurde gelöscht, wechsle zur neuesten');
+
+ // Direkter Supabase-Aufruf für die aktuellste Version
+ const { data: latestData, error: latestError } = await supabase
+ .from('documents')
+ .select('*')
+ .eq('conversation_id', actualConversationId)
+ .order('version', { ascending: false })
+ .limit(1)
+ .maybeSingle();
+
+ if (latestError) {
+ console.error('Fehler beim Laden des neuesten Dokuments:', latestError);
+ } else if (latestData) {
+ console.log('Setze neues aktuelles Dokument:', latestData.id);
+ setCurrentDocument(latestData);
+ } else {
+ console.log('Kein neuestes Dokument gefunden, setze null');
+ setCurrentDocument(null);
+ }
+ }
+
+ // Kurze Pause für bessere Benutzererfahrung
+ setTimeout(() => {
+ setIsVersionsModalVisible(false);
+
+ // Erfolgsmeldung anzeigen
+ Alert.alert(
+ "Version gelöscht",
+ `Die Dokumentversion ${document.version} wurde erfolgreich gelöscht.`,
+ [{ text: "OK" }]
+ );
+ }, 300);
+ } else {
+ console.error('Fehler beim Löschen der Dokumentversion');
+ Alert.alert('Fehler', 'Die Dokumentversion konnte nicht gelöscht werden.');
+ }
+ } catch (error) {
+ console.error('Fehler beim Löschen der Dokumentversion:', error);
+ if (error instanceof Error) {
+ console.error('Fehlerdetails:', error.message);
+ }
+ Alert.alert('Fehler', 'Die Dokumentversion konnte nicht gelöscht werden.');
+ } finally {
+ setIsDocumentLoading(false);
+ }
+ };
+
+ // Handler für die Navigation zur nächsten Version
+ const handleNextVersion = () => {
+ if (!currentDocument || documentVersions.length <= 1) return;
+
+ const currentIndex = documentVersions.findIndex(doc => doc.id === currentDocument.id);
+ if (currentIndex !== -1 && currentIndex > 0) {
+ // Versionen sind absteigend nach version sortiert (neueste zuerst)
+ setCurrentDocument(documentVersions[currentIndex - 1]);
+ }
+ };
+
+ // Handler für die Navigation zur vorherigen Version
+ const handlePreviousVersion = () => {
+ if (!currentDocument || documentVersions.length <= 1) return;
+
+ const currentIndex = documentVersions.findIndex(doc => doc.id === currentDocument.id);
+ if (currentIndex !== -1 && currentIndex < documentVersions.length - 1) {
+ // Versionen sind absteigend nach version sortiert (neueste zuerst)
+ setCurrentDocument(documentVersions[currentIndex + 1]);
+ }
+ };
+
+ // Fokussiere das Eingabefeld beim Laden der Seite
+ useEffect(() => {
+ // Kurze Verzögerung, um sicherzustellen, dass die Komponente vollständig geladen ist
+ const timer = setTimeout(() => {
+ if (messageInputRef.current) {
+ messageInputRef.current.focus();
+ }
+ }, 300);
+
+ return () => clearTimeout(timer);
+ }, []);
+
+ // Verarbeite die initiale Nachricht für neue Konversationen
+ useEffect(() => {
+ const handleInitialMessage = async () => {
+ if (isNewConversation && initialMsg && userId) {
+ try {
+ setIsLoading(true);
+
+ // Bestimme die zu verwendende Modell-ID
+ const selectedModelId = (!modelId || modelId === 'undefined')
+ ? '550e8400-e29b-41d4-a716-446655440000' // GPT-O3-Mini als Standard
+ : modelId as string;
+
+ console.log("Verarbeite initiale Nachricht mit Modell-ID:", selectedModelId);
+
+ // Erstelle eine neue Konversation
+ const newConversationId = await createConversation(
+ userId,
+ selectedModelId,
+ conversationMode as 'free' | 'guided' | 'template',
+ templateId,
+ isDocumentMode
+ );
+ console.log(`✓ Konversation mit ID=${newConversationId} für Modell=${selectedModelId} erstellt`);
+
+ console.log("Neue Konversation erstellt mit ID:", newConversationId);
+
+ if (newConversationId) {
+ setActualConversationId(newConversationId);
+
+ // Füge die initiale Nachricht hinzu
+ const userMessageId = await addMessage(newConversationId, 'user', initialMsg);
+ console.log("Neue Benutzernachricht hinzugefügt mit ID:", userMessageId);
+
+ if (userMessageId) {
+ // Füge die Nachricht zur UI hinzu
+ const userMessage: UIMessage = {
+ id: userMessageId,
+ text: initialMsg,
+ sender: 'user',
+ timestamp: new Date(),
+ };
+ setMessages([userMessage]);
+
+ // Füge zunächst einen Platzhalter mit Loading-Status hinzu
+ const tempAiMessage: UIMessage = {
+ id: `ai-temp-${Date.now()}`,
+ text: "",
+ sender: 'ai',
+ timestamp: new Date(),
+ isLoading: true,
+ };
+
+ setMessages((prev) => [...prev, tempAiMessage]);
+
+ // Hole die Antwort vom LLM
+ console.log("Sende Nachricht an LLM mit:", {
+ conversationId: newConversationId,
+ message: initialMsg,
+ modelId: selectedModelId,
+ documentMode: isDocumentMode
+ });
+
+ const { assistantResponse, title, documentContent } = await sendMessageAndGetResponse(
+ newConversationId,
+ initialMsg,
+ selectedModelId,
+ mode === 'template' ? templateId as string : undefined,
+ isDocumentMode
+ );
+
+ // Debug: Loggen des Dokumentinhalts
+ console.log("Dokumentmodus:", isDocumentMode);
+ console.log("Dokumentinhalt zurückerhalten:", documentContent ? "Ja" : "Nein");
+
+ // Wenn ein Dokumentinhalt zurückgegeben wurde, erstelle das erste Dokument
+ if (isDocumentMode && documentContent) {
+ console.log("Erstelle das erste Dokument mit Inhalt:", documentContent.substring(0, 50) + "...");
+ const docResult = await createDocument(newConversationId, documentContent);
+ console.log("Dokument erstellt:", docResult ? "Erfolgreich" : "Fehlgeschlagen");
+ await loadDocumentData(newConversationId);
+ } else if (isDocumentMode) {
+ console.log("Dokumentmodus ist aktiv, aber kein Dokumentinhalt wurde zurückgegeben");
+ }
+
+ // Wenn ein Titel zurückgegeben wurde, aktualisieren wir ihn
+ if (title) {
+ console.log("Titel für neue Konversation generiert:", title);
+ setConversationTitle(title);
+ }
+
+ console.log("LLM-Antwort erhalten:", assistantResponse.substring(0, 50) + "...");
+
+ // Ersetze den Platzhalter durch die echte Antwort
+ const aiMessage: UIMessage = {
+ id: `ai-${Date.now()}`,
+ text: assistantResponse,
+ sender: 'ai',
+ timestamp: new Date(),
+ };
+
+ // Ersetze den Platzhalter mit der echten Nachricht
+ setMessages((prev) =>
+ prev.map(msg =>
+ msg.id === tempAiMessage.id ? aiMessage : msg
+ )
+ );
+
+ // Aktualisiere die URL mit der neuen Konversations-ID
+ router.replace(`/conversation/${newConversationId}`);
+ }
+ }
+ } catch (error) {
+ console.error('Fehler beim Verarbeiten der initialen Nachricht:', error);
+ Alert.alert('Fehler', 'Die Nachricht konnte nicht verarbeitet werden.');
+ } finally {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ if (userId) {
+ handleInitialMessage();
+ }
+ }, [isNewConversation, initialMsg, userId, modelId, conversationMode, router]);
+
+ const handleSendMessage = async (text: string) => {
+ try {
+ console.log('handleSendMessage gestartet mit Text:', text.substring(0, 30) + '...');
+ if (!text.trim()) return;
+
+ // Prüfe, ob wir einen Benutzer haben
+ if (!userId) {
+ console.error('Fehler: Benutzer nicht verfügbar', { userId });
+ Alert.alert('Fehler', 'Du musst angemeldet sein, um Nachrichten zu senden.');
+ return;
+ }
+
+ // Falls wir kein Modell haben, aber eine bestehende Konversation, hole das Modell aus der Konversation
+ if ((!modelId || modelId === 'undefined') && !modelData && actualConversationId) {
+ try {
+ console.log('Hole Modell aus der Konversation:', actualConversationId);
+ const { data, error } = await supabase
+ .from('conversations')
+ .select('model_id')
+ .eq('id', actualConversationId)
+ .single();
+
+ if (error) {
+ console.error('Fehler beim Laden des Modells aus der Konversation:', error);
+ Alert.alert('Fehler', 'Modell konnte nicht geladen werden.');
+ return;
+ }
+
+ if (data && data.model_id) {
+ console.log('Modell-ID aus der Konversation geladen:', data.model_id);
+ const fetchedModelId = data.model_id;
+
+ // Setze das Modell für die nächsten API-Aufrufe
+ const response = await fetch(`/api/models`);
+ const models = await response.json();
+ const model = models.find((m: any) => m.id === fetchedModelId);
+
+ if (model) {
+ setModelName(model.name);
+ setModelData(model);
+ console.log('Model-Daten geladen:', model.name);
+ } else {
+ console.error('Modell nicht gefunden mit ID:', fetchedModelId);
+ Alert.alert('Fehler', 'Das Modell für diese Konversation wurde nicht gefunden.');
+ return;
+ }
+ } else {
+ console.error('Keine Modell-ID in der Konversation gefunden.');
+ Alert.alert('Fehler', 'Diese Konversation hat kein Modell.');
+ return;
+ }
+ } catch (modelError) {
+ console.error('Fehler beim Laden des Modells:', modelError);
+ Alert.alert('Fehler', 'Modell konnte nicht geladen werden.');
+ return;
+ }
+ } else {
+ console.log('Modell bereits vorhanden:', {
+ modelId,
+ modelName: modelData?.name || 'Unbekannt'
+ });
+ }
+
+ // Doppelprüfung, ob wir jetzt ein Modell haben
+ if (!modelId && !modelData) {
+ console.error('Fehler: Modell immer noch nicht verfügbar');
+ Alert.alert('Fehler', 'Kein Modell für die Konversation verfügbar.');
+ return;
+ }
+
+ console.log('Benutzer und Modell verfügbar:', {
+ userId,
+ modelId: modelId || (modelData && modelData.id),
+ modelName: modelData?.name || "Unbekannt"
+ });
+
+ // Neue Nachricht vom Benutzer hinzufügen (nur UI)
+ const tempUserMessage: UIMessage = {
+ id: `temp-${Date.now()}`,
+ text,
+ sender: 'user',
+ timestamp: new Date(),
+ };
+
+ setMessages((prev) => [...prev, tempUserMessage]);
+ setIsLoading(true);
+
+ // Wenn es eine neue Konversation ist, erstelle sie zuerst
+ let currentConversationId = actualConversationId;
+
+ if (!currentConversationId) {
+ currentConversationId = await createConversation(
+ userId,
+ modelId as string || (modelData && modelData.id),
+ conversationMode as 'free' | 'guided' | 'template',
+ templateId,
+ isDocumentMode
+ );
+
+ if (!currentConversationId) {
+ Alert.alert('Fehler', 'Konversation konnte nicht erstellt werden');
+ setIsLoading(false);
+ return;
+ }
+
+ setActualConversationId(currentConversationId);
+ }
+
+ // Sende die Nachricht und hole die Antwort
+ const selectedModelId = modelId as string || (modelData && modelData.id);
+ console.log('Sende Nachricht an sendMessageAndGetResponse mit:', {
+ conversationId: currentConversationId,
+ modelId: selectedModelId,
+ modelName: modelData?.name || "Unbekannt",
+ deployment: modelData?.parameters?.deployment || "Unbekannt",
+ documentMode: isDocumentMode
+ });
+
+ let assistantResponse = '';
+ try {
+ const result = await sendMessageAndGetResponse(
+ currentConversationId,
+ text,
+ selectedModelId,
+ mode === 'template' ? templateId as string : undefined,
+ isDocumentMode
+ );
+
+ assistantResponse = result.assistantResponse;
+ console.log('Antwort erhalten:', assistantResponse.substring(0, 50) + '...');
+
+ // Wenn ein Titel zurückgegeben wurde, aktualisieren wir ihn
+ if (result.title) {
+ console.log("Neuer Titel generiert:", result.title);
+ setConversationTitle(result.title);
+ }
+
+ // Debug: Dokumentmodus-Informationen loggen
+ console.log("Dokumentmodus:", isDocumentMode);
+ console.log("Dokumentinhalt erhalten:", result.documentContent ? "Ja" : "Nein");
+
+ // Wenn Dokumentinhalt zurückgegeben wurde und wir im Dokumentmodus sind
+ if (isDocumentMode && result.documentContent) {
+ console.log("Verarbeite Dokumentinhalt:", result.documentContent.substring(0, 50) + "...");
+
+ // Prüfen, ob bereits ein Dokument existiert
+ const hasExistingDoc = await hasDocument(currentConversationId);
+ console.log("Existierendes Dokument:", hasExistingDoc ? "Ja" : "Nein");
+
+ let docResult;
+ if (hasExistingDoc) {
+ // Neue Version erstellen
+ console.log("Erstelle neue Dokumentversion");
+ docResult = await createDocumentVersion(currentConversationId, result.documentContent);
+ } else {
+ // Neues Dokument erstellen
+ console.log("Erstelle neues Dokument");
+ docResult = await createDocument(currentConversationId, result.documentContent);
+ }
+
+ console.log("Dokument-Operation erfolgreich:", docResult ? "Ja" : "Nein");
+
+ // Dokument neu laden
+ await loadDocumentData(currentConversationId);
+ } else if (isDocumentMode) {
+ console.log("Dokumentmodus ist aktiv, aber kein Dokumentinhalt zurückgegeben");
+ }
+ } catch (sendError) {
+ console.error('Fehler in sendMessageAndGetResponse:', sendError);
+ throw sendError;
+ }
+
+ // Bevor wir die echte Antwort erhalten, zeigen wir einen Platzhalter mit Lade-Indikator
+ const tempAiMessage: UIMessage = {
+ id: `ai-temp-${Date.now()}`,
+ text: "", // Der Text ist leer, weil wir stattdessen den SkeletonLoader anzeigen
+ sender: 'ai',
+ timestamp: new Date(),
+ isLoading: true, // Zeigt an, dass dieser Nachricht noch geladen wird
+ };
+
+ setMessages((prev) => [...prev, tempAiMessage]);
+
+ // Wenn wir eine Antwort erhalten haben, ersetzen wir den Platzhalter mit der echten Antwort
+ if (assistantResponse) {
+ // Echte Antwort des Assistenten
+ const aiMessage: UIMessage = {
+ id: `ai-${Date.now()}`,
+ text: assistantResponse,
+ sender: 'ai',
+ timestamp: new Date(),
+ };
+
+ // Ersetze den Platzhalter mit der echten Nachricht
+ setMessages((prev) =>
+ prev.map(msg =>
+ msg.id === tempAiMessage.id ? aiMessage : msg
+ )
+ );
+ console.log('Assistentenantwort zur UI hinzugefügt');
+ }
+
+ // Behalte die aktuelle Message-Reihenfolge bei und ersetze nur die temporären IDs
+ console.log('Aktualisiere Nachrichten mit korrekten IDs...');
+ const dbMessages = await getMessages(currentConversationId);
+ console.log(`${dbMessages.length} Nachrichten aus der Datenbank geladen`);
+
+ // In diesem Fall ersetzen wir nicht die gesamten Nachrichten, damit der Kontext erhalten bleibt
+ // und der Benutzer auf seiner aktuellen Position in der Konversation bleibt
+ // setMessages(convertDbToUiMessages(dbMessages));
+
+ // Wenn es eine neue Konversation war, aktualisiere die URL
+ if (isNewConversation && currentConversationId) {
+ router.replace(`/conversation/${currentConversationId}`);
+ }
+ } catch (error) {
+ console.error('Fehler beim Senden der Nachricht:', error);
+
+ // Detaillierte Fehlerinformationen ausgeben
+ if (error instanceof Error) {
+ console.error('Fehlerdetails:', {
+ name: error.name,
+ message: error.message,
+ stack: error.stack
+ });
+ }
+
+ Alert.alert('Fehler', `Die Nachricht konnte nicht gesendet werden: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`);
+ } finally {
+ setIsLoading(false);
+ console.log('handleSendMessage abgeschlossen');
+ }
+ };
+
+ return (
+
+
+ {/* Drawer / Seitenmenü */}
+ {isDrawerOpen && (
+
+ setIsDrawerOpen(false)}
+ />
+
+ )}
+
+ {/* Hauptinhalt */}
+
+
+
+ setIsDrawerOpen(!isDrawerOpen)}
+ >
+
+
+
+
+ router.back()}
+ >
+
+
+
+
+
+
+
+ {/* Dokumentmodus Layout */}
+ {isDocumentMode ? (
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+ /* Standard-Layout ohne Dokumentmodus */
+ <>
+
+
+
+
+
+ >
+ )}
+
+
+
+
+ {/* Versionen Modal */}
+ setIsVersionsModalVisible(false)}
+ onSelectVersion={handleSelectVersion}
+ onDeleteVersion={handleDeleteVersion}
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ mainLayout: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+ mainContainer: {
+ flex: 1,
+ alignItems: 'center',
+ },
+ drawerContainer: {
+ width: 260,
+ height: '100%',
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ bottom: 0,
+ zIndex: 10,
+ },
+ keyboardContainer: {
+ flex: 1,
+ width: '100%',
+ maxWidth: 1200,
+ },
+ headerContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ width: '100%',
+ },
+ menuButton: {
+ padding: 12,
+ marginRight: 0,
+ zIndex: 5,
+ },
+ headerContentContainer: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ backButton: {
+ padding: 8,
+ marginRight: 8,
+ },
+ messageContainer: {
+ flex: 1,
+ width: '100%',
+ },
+ // Dokumentmodus-Styles
+ documentLayout: {
+ flex: 1,
+ flexDirection: 'row',
+ width: '100%',
+ height: '100%',
+ },
+ documentMessageContainer: {
+ width: '50%',
+ borderRightWidth: 1,
+ borderRightColor: 'rgba(0,0,0,0.1)',
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ documentPanelContainer: {
+ width: '50%',
+ height: '100%',
+ paddingTop: 8,
+ paddingBottom: 16,
+ },
+});
diff --git a/chat/apps/mobile/app/conversation/new/index.tsx b/chat/apps/mobile/app/conversation/new/index.tsx
new file mode 100644
index 000000000..00dcac0e4
--- /dev/null
+++ b/chat/apps/mobile/app/conversation/new/index.tsx
@@ -0,0 +1,129 @@
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import { useEffect, useState } from 'react';
+import { View, ActivityIndicator, StyleSheet, Text } from 'react-native';
+import { createConversation, sendMessageAndGetResponse } from '../../../services/conversation';
+import { useAuth } from '../../../context/AuthProvider';
+import { Alert } from 'react-native';
+
+// Typendefinition für Parameter
+interface ConversationNewParams {
+ initialMessage?: string;
+ modelId?: string;
+ templateId?: string;
+ mode?: 'free' | 'guided' | 'template';
+ documentMode?: string; // String, da Query-Parameter immer Strings sind
+ spaceId?: string; // ID des Space, falls vorhanden
+}
+
+export default function NewConversation() {
+ const { user } = useAuth();
+ const router = useRouter();
+ const params = useLocalSearchParams();
+ const [isFetching, setIsFetching] = useState(true);
+
+ // Extrahiere die Parameter
+ const initialMessage = params?.initialMessage || '';
+ const modelId = params?.modelId || '550e8400-e29b-41d4-a716-446655440000'; // Default zu GPT-4o-mini
+ const templateId = params?.templateId;
+ const mode = (params?.mode || 'free') as 'free' | 'guided' | 'template';
+ const documentMode = params?.documentMode === 'true';
+ const spaceId = params?.spaceId;
+
+ console.log('Erhaltene Parameter:', {
+ initialMessage: initialMessage.substring(0, 50),
+ modelId,
+ templateId,
+ mode,
+ documentMode,
+ spaceId: spaceId || 'nicht angegeben'
+ });
+
+ // Log für Debug-Zwecke
+ console.log("⭐️ Neue Konversation wird erstellt mit Space ID:", spaceId || "keine");
+
+ useEffect(() => {
+ if (!user) {
+ console.error('Kein Benutzer gefunden');
+ router.replace('/auth/login');
+ return;
+ }
+
+ if (!initialMessage) {
+ console.warn('Keine Nachricht gefunden');
+ router.replace('/');
+ return;
+ }
+
+ const startConversation = async () => {
+ try {
+ setIsFetching(true);
+ console.log('Erstelle Konversation...');
+
+ // 1. Erstelle eine neue Konversation
+ const conversationId = await createConversation(
+ user.id,
+ modelId,
+ mode,
+ templateId,
+ documentMode,
+ spaceId
+ );
+
+ if (!conversationId) {
+ throw new Error('Fehler beim Erstellen der Konversation');
+ }
+
+ console.log('Konversation erstellt mit ID:', conversationId);
+
+ // 2. Sende die initiale Nachricht
+ const response = await sendMessageAndGetResponse(
+ conversationId,
+ initialMessage,
+ modelId,
+ templateId,
+ documentMode
+ );
+
+ console.log('Antwort erhalten');
+
+ // 3. Navigiere zur Konversation
+ router.replace(`/conversation/${conversationId}`);
+ } catch (error) {
+ console.error('Fehler beim Starten der Konversation:', error);
+ Alert.alert(
+ 'Fehler',
+ 'Die Konversation konnte nicht gestartet werden.',
+ [
+ {
+ text: 'OK',
+ onPress: () => router.replace('/')
+ }
+ ]
+ );
+ } finally {
+ setIsFetching(false);
+ }
+ };
+
+ startConversation();
+ }, [user, initialMessage, modelId, templateId, mode, documentMode, spaceId, router]);
+
+ return (
+
+
+ Starte Konversation...
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ text: {
+ marginTop: 20,
+ fontSize: 16,
+ }
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/app/conversations.tsx b/chat/apps/mobile/app/conversations.tsx
new file mode 100644
index 000000000..77e23fb92
--- /dev/null
+++ b/chat/apps/mobile/app/conversations.tsx
@@ -0,0 +1,604 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ FlatList,
+ SafeAreaView,
+ Alert,
+ ActivityIndicator,
+ Pressable,
+ Platform,
+ Dimensions
+} from 'react-native';
+import { useTheme, useFocusEffect } from '@react-navigation/native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { useAuth } from '../context/AuthProvider';
+import { useAppTheme } from '../theme/ThemeProvider';
+import CustomDrawer from '../components/CustomDrawer';
+import {
+ getConversations,
+ getMessages,
+ deleteConversation,
+ archiveConversation
+} from '../services/conversation';
+import { supabase } from '../utils/supabase';
+
+// Typendefinitionen für Konversationen
+type ConversationItem = {
+ id: string;
+ modelName: string;
+ title: string;
+ lastMessage: string;
+ timestamp: Date;
+ mode: 'frei' | 'geführt' | 'vorlage';
+};
+
+// Hilfsfunktion zur Formatierung des Datums
+const formatDate = (date: Date) => {
+ const day = date.getDate().toString().padStart(2, '0');
+ const month = new Intl.DateTimeFormat('de-DE', { month: 'short' }).format(date);
+ const hours = date.getHours().toString().padStart(2, '0');
+ const minutes = date.getMinutes().toString().padStart(2, '0');
+ return `${day}. ${month}, ${hours}:${minutes}`;
+};
+
+export default function ConversationsScreen() {
+ const { colors } = useTheme();
+ const router = useRouter();
+ const { user } = useAuth();
+ const [conversations, setConversations] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const { isDarkMode } = useAppTheme();
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false);
+
+ // Eine Funktion, die Konversationen lädt und wiederverwendet werden kann
+ const loadConversations = async () => {
+ if (!user) return;
+
+ setIsLoading(true);
+ try {
+ console.log("Lade Konversationen für User:", user.id);
+ // Lade alle nicht-archivierten Konversationen des Benutzers
+ const userConversations = await getConversations(user.id);
+ console.log(`${userConversations.length} Konversationen geladen`, new Date().toLocaleTimeString());
+
+ // Lade für jede Konversation die letzte Nachricht und das Modell
+ const conversationItems: ConversationItem[] = [];
+
+ for (const conv of userConversations) {
+ try {
+ // Lade die Nachrichten der Konversation
+ const messages = await getMessages(conv.id);
+ // Lade das Modell aus der Datenbank
+ const { data: modelData } = await supabase
+ .from('models')
+ .select('name')
+ .eq('id', conv.model_id)
+ .single();
+
+ // Finde die letzte Nachricht (die nicht vom System ist)
+ const lastMessage = messages
+ .filter(msg => msg.sender !== 'system')
+ .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
+
+ if (lastMessage) {
+ conversationItems.push({
+ id: conv.id,
+ modelName: modelData?.name || 'Unbekanntes Modell',
+ title: conv.title || 'Unbenannte Konversation',
+ lastMessage: lastMessage.message_text,
+ timestamp: new Date(conv.updated_at),
+ mode: conv.conversation_mode === 'free' ? 'frei' :
+ conv.conversation_mode === 'guided' ? 'geführt' : 'vorlage'
+ });
+ }
+ } catch (error) {
+ console.error(`Fehler beim Laden der Details für Konversation ${conv.id}:`, error);
+ }
+ }
+
+ setConversations(conversationItems);
+ } catch (error) {
+ console.error('Fehler beim Laden der Konversationen:', error);
+ Alert.alert('Fehler', 'Die Konversationen konnten nicht geladen werden.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Lade die Konversationen beim ersten Rendern und wenn sich der User ändert
+ useEffect(() => {
+ loadConversations();
+ }, [user]);
+
+ // Lade Konversationen erneut, wenn der Screen fokussiert wird
+ useFocusEffect(
+ useCallback(() => {
+ if (user) loadConversations();
+ return () => {};
+ }, [user])
+ );
+
+ const handleConversationPress = (id: string) => {
+ // Navigiere zum Konversations-Screen mit der ID
+ router.push(`/conversation/${id}`);
+ };
+
+ // Löschen einer Konversation
+ const handleDeleteConversation = (id: string) => {
+ Alert.alert(
+ "Konversation löschen",
+ "Möchtest du diese Konversation wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
+ [
+ {
+ text: "Abbrechen",
+ style: "cancel"
+ },
+ {
+ text: "Löschen",
+ style: "destructive",
+ onPress: async () => {
+ try {
+ const success = await deleteConversation(id);
+ if (success) {
+ // Aus der lokalen Liste entfernen
+ setConversations(prev => prev.filter(conv => conv.id !== id));
+ Alert.alert("Erfolg", "Die Konversation wurde gelöscht.");
+ } else {
+ Alert.alert("Fehler", "Die Konversation konnte nicht gelöscht werden.");
+ }
+ } catch (error) {
+ console.error('Fehler beim Löschen der Konversation:', error);
+ Alert.alert("Fehler", "Die Konversation konnte nicht gelöscht werden.");
+ }
+ }
+ }
+ ]
+ );
+ };
+
+ // Archivieren einer Konversation
+ const handleArchiveConversation = async (id: string) => {
+ try {
+ const success = await archiveConversation(id);
+ if (success) {
+ // Aus der lokalen Liste entfernen
+ setConversations(prev => prev.filter(conv => conv.id !== id));
+ Alert.alert("Erfolg", "Die Konversation wurde archiviert.");
+ } else {
+ Alert.alert("Fehler", "Die Konversation konnte nicht archiviert werden.");
+ }
+ } catch (error) {
+ console.error('Fehler beim Archivieren der Konversation:', error);
+ Alert.alert("Fehler", "Die Konversation konnte nicht archiviert werden.");
+ }
+ };
+
+ // Zustandsverwaltung für die Optionsmenüs der Konversationselemente
+ const [expandedConversationId, setExpandedConversationId] = useState(null);
+
+ // Toggle-Funktion für das Optionsmenü
+ const toggleOptionsMenu = (id: string) => {
+ setExpandedConversationId(expandedConversationId === id ? null : id);
+ };
+
+ const renderConversationItem = ({ item }: { item: ConversationItem }) => {
+ const showOptions = expandedConversationId === item.id;
+
+ return (
+
+ [
+ styles.conversationItem,
+ hovered && { backgroundColor: colors.cardHover },
+ pressed && { opacity: 0.9 }
+ ]}
+ onPress={() => handleConversationPress(item.id)}
+ onLongPress={() => toggleOptionsMenu(item.id)}
+ >
+ {({ pressed, hovered }) => (
+ <>
+
+
+
+
+
+ {item.title}
+
+
+
+
+
+
+
+ {item.modelName}
+
+
+
+
+
+ {item.mode === 'frei' ? 'Frei' :
+ item.mode === 'geführt' ? 'Geführt' : 'Vorlage'}
+
+
+
+
+ {formatDate(item.timestamp)}
+
+
+
+
+ {item.lastMessage}
+
+
+
+ [
+ styles.optionsButton,
+ hovered && { backgroundColor: colors.menuItemHover },
+ pressed && { opacity: 0.7 }
+ ]}
+ onPress={() => toggleOptionsMenu(item.id)}
+ >
+ {({ pressed, hovered }) => (
+
+ )}
+
+ >
+ )}
+
+
+ {showOptions && (
+
+ [
+ styles.optionButton,
+ hovered && { backgroundColor: colors.menuItemHover },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={() => handleArchiveConversation(item.id)}
+ >
+ {({ pressed, hovered }) => (
+ <>
+
+ Archivieren
+ >
+ )}
+
+
+ [
+ styles.optionButton,
+ hovered && { backgroundColor: colors.dangerHover },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={() => handleDeleteConversation(item.id)}
+ >
+ {({ pressed, hovered }) => (
+ <>
+
+ Löschen
+ >
+ )}
+
+
+ )}
+
+ );
+ };
+
+ return (
+
+
+ {/* Permanenter Drawer links */}
+ {isDrawerOpen && (
+
+ setIsDrawerOpen(false)}
+ />
+
+ )}
+
+ {/* Hauptinhalt */}
+
+
+
+ [
+ styles.menuButton,
+ hovered && { backgroundColor: colors.menuItemHover },
+ pressed && { opacity: 0.7 }
+ ]}
+ onPress={() => setIsDrawerOpen(!isDrawerOpen)}
+ >
+ {({ pressed, hovered }) => (
+
+ )}
+
+
+ Konversationen
+
+
+ {/* Konversationsliste */}
+
+ {isLoading ? (
+
+
+
+ Konversationen werden geladen...
+
+
+ ) : conversations.length > 0 ? (
+ item.id}
+ renderItem={renderConversationItem}
+ contentContainerStyle={styles.listContent}
+ numColumns={Platform.OS === 'web' ? Math.min(Math.floor((Dimensions.get('window').width - 32) / 400), 3) : 1}
+ key={Platform.OS === 'web' ? Math.min(Math.floor((Dimensions.get('window').width - 32) / 400), 3) : 1}
+ />
+ ) : (
+
+
+
+ Keine Konversationen vorhanden
+
+
+ Starte eine neue Konversation über den Hauptbildschirm
+
+
+ )}
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ mainLayout: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+ mainContainer: {
+ flex: 1,
+ alignItems: 'center',
+ },
+ drawerContainer: {
+ width: 260,
+ height: '100%',
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ bottom: 0,
+ zIndex: 10,
+ },
+ contentContainer: {
+ flex: 1,
+ maxWidth: 1200,
+ width: '100%',
+ },
+ headerContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ paddingTop: 16,
+ paddingBottom: 8,
+ zIndex: 10, // Stelle sicher, dass der Header über allem anderen liegt
+ elevation: 10, // Für Android
+ },
+ menuButton: {
+ padding: 10,
+ marginRight: 12,
+ zIndex: 5,
+ borderRadius: 20,
+ width: 40,
+ height: 40,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ headerTitle: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ },
+ listContainer: {
+ flex: 1,
+ width: '100%',
+ paddingHorizontal: 16,
+ },
+ listContent: {
+ paddingBottom: 20,
+ paddingTop: 12,
+ gap: 16,
+ alignSelf: 'center',
+ justifyContent: Platform.OS === 'web' ? 'flex-start' : undefined,
+ },
+ conversationItemWrapper: {
+ borderRadius: 12,
+ overflow: 'hidden',
+ margin: 8,
+ width: Platform.OS === 'web' ? 380 : undefined,
+ ...Platform.select({
+ ios: {
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ },
+ android: {
+ elevation: 3,
+ },
+ web: {
+ boxShadow: '0 2px 6px rgba(0, 0, 0, 0.1)',
+ },
+ }),
+ },
+ conversationItem: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ padding: 16,
+ },
+ conversationContent: {
+ flex: 1,
+ display: 'flex',
+ flexDirection: 'column',
+ height: '100%',
+ },
+ optionsContainer: {
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ paddingHorizontal: 16,
+ paddingBottom: 12,
+ paddingTop: 8,
+ },
+ optionButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ marginLeft: 8,
+ borderRadius: 6,
+ },
+ optionText: {
+ fontSize: 14,
+ marginLeft: 6,
+ fontWeight: '500',
+ },
+ conversationHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 6,
+ },
+ titleRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ },
+ titleIcon: {
+ marginRight: 8,
+ },
+ title: {
+ fontSize: 16,
+ fontWeight: '600',
+ flex: 1,
+ marginBottom: 2,
+ },
+ badgeContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 10,
+ gap: 8,
+ flexWrap: 'wrap',
+ },
+ modelBadge: {
+ paddingHorizontal: 8,
+ paddingVertical: 3,
+ borderRadius: 12,
+ },
+ modelName: {
+ fontSize: 12,
+ fontWeight: '500',
+ },
+ modeBadge: {
+ paddingHorizontal: 8,
+ paddingVertical: 3,
+ borderRadius: 12,
+ },
+ timestamp: {
+ fontSize: 11,
+ marginLeft: 'auto', // Um es an den rechten Rand zu schieben
+ },
+ lastMessage: {
+ fontSize: 14,
+ marginBottom: 6,
+ lineHeight: 20,
+ marginTop: 4,
+ flex: 1, // Damit die Nachricht den verbleibenden Platz einnimmt
+ },
+ modeText: {
+ fontSize: 11,
+ fontWeight: '500',
+ },
+ optionsButton: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ // Container für den Ladezustand
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 32,
+ marginTop: 40,
+ },
+ loadingText: {
+ fontSize: 16,
+ marginTop: 16,
+ textAlign: 'center',
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 32,
+ marginTop: 40,
+ },
+ emptyText: {
+ fontSize: 18,
+ fontWeight: '600',
+ marginTop: 16,
+ textAlign: 'center',
+ },
+ emptySubtext: {
+ fontSize: 14,
+ textAlign: 'center',
+ marginTop: 8,
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/app/documents.tsx b/chat/apps/mobile/app/documents.tsx
new file mode 100644
index 000000000..b86bf5167
--- /dev/null
+++ b/chat/apps/mobile/app/documents.tsx
@@ -0,0 +1,465 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ ActivityIndicator,
+ useWindowDimensions,
+ Platform
+} from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { Document } from '../services/document';
+import { supabase } from '../utils/supabase';
+import Markdown from 'react-native-markdown-display';
+
+type DocumentWithTitle = Document & {
+ conversation_title: string;
+};
+
+export default function DocumentsScreen() {
+ const { colors } = useTheme();
+ const router = useRouter();
+ const { width } = useWindowDimensions();
+ const [documents, setDocuments] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [userId, setUserId] = useState(null);
+
+ // Berechne die Anzahl der Spalten basierend auf der Bildschirmbreite
+ const columnsCount = useMemo(() => {
+ // Mobile (schmaler Bildschirm)
+ if (width < 600) {
+ return 1;
+ }
+ // Tablet
+ if (width < 1100) {
+ return 2;
+ }
+ // Desktop oder großes Tablet
+ return 3;
+ }, [width]);
+
+ // Berechne die Breite jeder Karte basierend auf der Spaltenanzahl
+ const cardWidth = useMemo(() => {
+ const padding = 16; // Container-Padding rechts und links
+ const gap = 16; // Abstand zwischen Karten
+ const contentWidth = width - (padding * 2);
+ const gapTotal = gap * (columnsCount - 1);
+ const availableWidth = contentWidth - gapTotal;
+
+ // Verhältnis für schmalere Karten, je nach Spaltenanzahl anpassen
+ const widthRatio = columnsCount === 1 ? 0.95 : // Fast volle Breite bei 1 Spalte
+ columnsCount === 2 ? 0.48 : // Etwas schmaler bei 2 Spalten
+ 0.31; // Noch schmaler bei 3 Spalten
+
+ return (availableWidth * widthRatio);
+ }, [width, columnsCount]);
+
+ useEffect(() => {
+ const checkUser = async () => {
+ const { data } = await supabase.auth.getUser();
+ if (data?.user) {
+ setUserId(data.user.id);
+ } else {
+ // In einer echten App würden wir hier zur Login-Seite weiterleiten
+ // Für jetzt verwenden wir eine Test-ID
+ setUserId('test-user-id');
+ }
+ };
+ checkUser();
+ }, []);
+
+ useEffect(() => {
+ if (userId) {
+ loadDocuments();
+ }
+ }, [userId]);
+
+ const loadDocuments = async () => {
+ try {
+ setIsLoading(true);
+
+ // Lade alle Konversationen des Benutzers, die im Dokumentmodus sind
+ const { data: conversations, error: convError } = await supabase
+ .from('conversations')
+ .select('id, title, document_mode')
+ .eq('user_id', userId)
+ .eq('document_mode', true);
+
+ if (convError) {
+ console.error('Fehler beim Laden der Konversationen:', convError);
+ setIsLoading(false);
+ return;
+ }
+
+ if (!conversations || conversations.length === 0) {
+ setDocuments([]);
+ setIsLoading(false);
+ return;
+ }
+
+ // Für jede Konversation den neuesten Dokumentstand laden
+ const latestDocuments: DocumentWithTitle[] = [];
+
+ for (const conv of conversations) {
+ const { data: docData, error: docError } = await supabase
+ .from('documents')
+ .select('*')
+ .eq('conversation_id', conv.id)
+ .order('version', { ascending: false })
+ .limit(1)
+ .single();
+
+ if (docError) {
+ if (docError.code !== 'PGRST116') { // Ignore "No rows found" error
+ console.error(`Fehler beim Laden des Dokuments für Konversation ${conv.id}:`, docError);
+ }
+ continue;
+ }
+
+ if (docData) {
+ latestDocuments.push({
+ ...docData,
+ conversation_title: conv.title || 'Unbenannte Konversation'
+ });
+ }
+ }
+
+ setDocuments(latestDocuments);
+ } catch (error) {
+ console.error('Fehler beim Laden der Dokumente:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const navigateToConversation = (conversationId: string) => {
+ router.push(`/conversation/${conversationId}`);
+ };
+
+ // Funktion zum Extrahieren eines Titels aus dem Dokumentinhalt
+ const extractDocumentTitle = (content: string): string => {
+ // Suche nach einer Markdown-Überschrift Ebene 1 am Anfang
+ const titleMatch = content.match(/^#\s+(.+)$/m);
+ if (titleMatch && titleMatch[1]) {
+ return titleMatch[1].trim();
+ }
+
+ // Alternativ: Suche nach einer Markdown-Überschrift Ebene 2
+ const subtitleMatch = content.match(/^##\s+(.+)$/m);
+ if (subtitleMatch && subtitleMatch[1]) {
+ return subtitleMatch[1].trim();
+ }
+
+ // Wenn keine Überschrift gefunden wurde, nimm die ersten Wörter
+ const firstLine = content.split('\n')[0].trim();
+ if (firstLine.length > 0) {
+ return firstLine.length > 40 ? `${firstLine.substring(0, 37)}...` : firstLine;
+ }
+
+ return 'Dokument ohne Titel';
+ };
+
+ // Funktion zum Entfernen nur der ersten H1-Überschrift aus dem Inhalt
+ const removeHeadingFromContent = (content: string, title: string): string => {
+ // Prüfe, ob das Dokument mit einer H1-Überschrift beginnt
+ const firstLineMatch = content.match(/^#\s+(.+)$/m);
+
+ if (firstLineMatch && firstLineMatch.index === 0) {
+ // Entferne nur die erste H1-Überschrift am Anfang des Dokuments
+ const parts = content.split('\n');
+ parts.shift(); // Entferne die erste Zeile (H1-Überschrift)
+
+ // Entferne leere Zeilen am Anfang
+ let modifiedContent = parts.join('\n').replace(/^\s+/, '');
+
+ return modifiedContent;
+ }
+
+ // Wenn keine H1-Überschrift am Anfang gefunden wurde,
+ // gib den ursprünglichen Inhalt zurück
+ return content;
+ };
+
+ // Funktion zum Formatieren des Datums
+ const formatDate = (dateString: string): string => {
+ const date = new Date(dateString);
+ const day = date.getDate().toString().padStart(2, '0');
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
+ const year = date.getFullYear();
+ return `${day}.${month}.${year}`;
+ };
+
+ return (
+
+
+ router.back()}>
+
+
+ Alle Dokumente
+
+
+
+
+
+ {isLoading ? (
+
+
+
+ Dokumente werden geladen...
+
+
+ ) : documents.length === 0 ? (
+
+
+
+ Keine Dokumente gefunden
+
+
+ Erstelle ein neues Dokument in einer Konversation mit aktiviertem Dokumentmodus.
+
+
+ ) : (
+
+ {documents.map((doc) => (
+ navigateToConversation(doc.conversation_id)}
+ >
+
+
+ {extractDocumentTitle(doc.content)}
+
+
+
+ {doc.conversation_title}
+
+
+
+ {formatDate(doc.updated_at)}
+
+
+ v{doc.version}
+
+
+
+
+
+
+
+ {removeHeadingFromContent(doc.content, extractDocumentTitle(doc.content))}
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 16,
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(0,0,0,0.1)',
+ },
+ backButton: {
+ padding: 6,
+ },
+ headerTitle: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ flex: 1,
+ paddingLeft: 12,
+ },
+ refreshButton: {
+ padding: 6,
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ loadingText: {
+ marginTop: 12,
+ fontSize: 16,
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
+ },
+ emptyIcon: {
+ marginBottom: 20,
+ opacity: 0.6,
+ },
+ emptyText: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+ emptySubtext: {
+ fontSize: 14,
+ textAlign: 'center',
+ opacity: 0.7,
+ maxWidth: '80%',
+ },
+ scrollContainer: {
+ flex: 1,
+ },
+ documentsContainer: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ padding: 16,
+ // In einem flexiblen Layout nicht mehr space-between verwenden
+ // sondern einen festen Abstand zwischen Items
+ gap: 20,
+ // Alignment um die Karten horizontal zu zentrieren
+ justifyContent: 'center'
+ },
+ documentCard: {
+ // width wird dynamisch basierend auf columnsCount berechnet
+ borderRadius: 12,
+ borderWidth: 1,
+ overflow: 'hidden',
+ display: 'flex',
+ flexDirection: 'column',
+ // Shadow für die Karten hinzufügen
+ ...Platform.select({
+ ios: {
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ },
+ android: {
+ elevation: 3,
+ },
+ web: {
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
+ },
+ }),
+ },
+ documentHeader: {
+ padding: 16,
+ paddingBottom: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(0,0,0,0.1)',
+ },
+ documentTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ lineHeight: 22,
+ },
+ documentMeta: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingRight: 8,
+ },
+ conversationTitle: {
+ fontSize: 12,
+ opacity: 0.7,
+ flex: 1,
+ },
+ metaRight: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ documentDate: {
+ fontSize: 11,
+ opacity: 0.7,
+ },
+ documentVersion: {
+ fontSize: 12,
+ fontWeight: 'bold',
+ backgroundColor: 'rgba(0,0,0,0.1)',
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ borderRadius: 10,
+ },
+ contentContainer: {
+ flex: 1,
+ // Vorschau-Bereich kleiner machen
+ maxHeight: 180,
+ },
+ documentContent: {
+ padding: 12,
+ // Zusätzliche Eigenschaften für einen besseren Vorschaubereich
+ paddingTop: 8,
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/app/index.tsx b/chat/apps/mobile/app/index.tsx
new file mode 100644
index 000000000..c04eaa7a3
--- /dev/null
+++ b/chat/apps/mobile/app/index.tsx
@@ -0,0 +1,905 @@
+import React, { useState, useEffect, useCallback, useRef } from 'react';
+import { View, Text, StyleSheet, FlatList, TouchableOpacity, SafeAreaView, Alert, ActivityIndicator, TextInput, Pressable, Platform, ScrollView } from 'react-native';
+import { useTheme, useFocusEffect } from '@react-navigation/native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { useAuth } from '../context/AuthProvider';
+
+import NewChatButton from '../components/NewChatButton';
+import ConversationStarter, { ConversationStarterRef } from '../components/ConversationStarter';
+import CustomDrawer from '../components/CustomDrawer';
+import { useAppTheme } from '../theme/ThemeProvider';
+import { getConversations, getMessages, deleteConversation, archiveConversation } from '../services/conversation';
+import { getUserSpaces, Space } from '../services/space';
+import { supabase } from '../utils/supabase';
+
+// Typendefinitionen für Konversationen
+type ConversationItem = {
+ id: string;
+ modelName: string;
+ title: string;
+ lastMessage: string;
+ timestamp: Date;
+ mode: 'frei' | 'geführt' | 'vorlage';
+};
+
+// Hilfsfunktion zur Formatierung des Datums
+const formatDate = (date: Date) => {
+ const day = date.getDate().toString().padStart(2, '0');
+ const month = new Intl.DateTimeFormat('de-DE', { month: 'short' }).format(date);
+ const hours = date.getHours().toString().padStart(2, '0');
+ const minutes = date.getMinutes().toString().padStart(2, '0');
+ return `${day}. ${month}, ${hours}:${minutes}`;
+};
+
+export default function HomeScreen() {
+ const { colors } = useTheme();
+ const router = useRouter();
+ const { user, signOut } = useAuth();
+ const [conversations, setConversations] = useState([]);
+ const [spaces, setSpaces] = useState([]);
+ const [selectedSpaceId, setSelectedSpaceId] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isLoadingSpaces, setIsLoadingSpaces] = useState(true);
+ const { isDarkMode } = useAppTheme();
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false);
+ const chatInputRef = useRef(null);
+
+ // Eine Funktion, die Konversationen lädt und wiederverwendet werden kann
+ // Fokussiere das Eingabefeld beim ersten Laden
+ useEffect(() => {
+ // Kurze Verzögerung, um sicherzustellen, dass die Komponente vollständig gerendert ist
+ setTimeout(() => {
+ if (chatInputRef.current) {
+ chatInputRef.current.focus();
+ }
+ }, 300);
+ }, []);
+
+ const loadConversations = async () => {
+ if (!user) return;
+
+ setIsLoading(true);
+ try {
+ console.log("Lade Konversationen für User:", user.id);
+ console.log("Selected Space ID:", selectedSpaceId || "Alle Spaces");
+
+ // Lade Konversationen des Benutzers, gefiltert nach Space wenn ausgewählt
+ const userConversations = await getConversations(user.id, selectedSpaceId || undefined);
+ console.log(`${userConversations.length} Konversationen geladen`, new Date().toLocaleTimeString());
+
+ // Lade für jede Konversation die letzte Nachricht und das Modell
+ const conversationItems: ConversationItem[] = [];
+
+ for (const conv of userConversations) {
+ try {
+ // Lade die Nachrichten der Konversation
+ const messages = await getMessages(conv.id);
+ // Lade das Modell aus der Datenbank
+ const { data: modelData } = await supabase
+ .from('models')
+ .select('name')
+ .eq('id', conv.model_id)
+ .single();
+
+ // Finde die letzte Nachricht (die nicht vom System ist)
+ const lastMessage = messages
+ .filter(msg => msg.sender !== 'system')
+ .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
+
+ if (lastMessage) {
+ conversationItems.push({
+ id: conv.id,
+ modelName: modelData?.name || 'Unbekanntes Modell',
+ title: conv.title || 'Unbenannte Konversation',
+ lastMessage: lastMessage.message_text,
+ timestamp: new Date(conv.updated_at),
+ mode: conv.conversation_mode === 'free' ? 'frei' :
+ conv.conversation_mode === 'guided' ? 'geführt' : 'vorlage'
+ });
+ }
+ } catch (error) {
+ console.error(`Fehler beim Laden der Details für Konversation ${conv.id}:`, error);
+ }
+ }
+
+ setConversations(conversationItems);
+ } catch (error) {
+ console.error('Fehler beim Laden der Konversationen:', error);
+ Alert.alert('Fehler', 'Die Konversationen konnten nicht geladen werden.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Lade Spaces
+ const loadSpaces = useCallback(async () => {
+ if (!user) return;
+
+ setIsLoadingSpaces(true);
+ try {
+ const userSpaces = await getUserSpaces(user.id);
+ setSpaces(userSpaces);
+ } catch (error) {
+ console.error('Fehler beim Laden der Spaces:', error);
+ } finally {
+ setIsLoadingSpaces(false);
+ }
+ }, [user]);
+
+ // Lade die Konversationen beim ersten Rendern und wenn sich der User oder selectedSpaceId ändert
+ useEffect(() => {
+ loadConversations();
+ }, [user, selectedSpaceId]);
+
+ // Lade Spaces beim ersten Rendern
+ useEffect(() => {
+ loadSpaces();
+ }, [loadSpaces]);
+
+ // Lade Konversationen und Spaces erneut, wenn der Screen fokussiert wird
+ useFocusEffect(
+ useCallback(() => {
+ if (user) {
+ loadConversations();
+ loadSpaces();
+ }
+ return () => {};
+ }, [user, loadSpaces, selectedSpaceId])
+ );
+
+ // Space auswählen
+ const handleSpaceSelect = (spaceId: string | null) => {
+ console.log("Space ausgewählt:", spaceId);
+ setSelectedSpaceId(spaceId);
+
+ // Alert für Debug-Zwecke
+ Alert.alert(
+ "Space ausgewählt",
+ `Space ID: ${spaceId || 'Alle Spaces'}`
+ );
+ };
+
+ const handleNewChat = () => {
+ // Navigiere zum Modellauswahl-Screen
+ router.push('/model-selection');
+ };
+
+ const handleLogout = async () => {
+ try {
+ await signOut();
+ router.replace('/auth/login');
+ } catch (error) {
+ console.error('Fehler beim Abmelden:', error);
+ Alert.alert('Fehler', 'Bei der Abmeldung ist ein Fehler aufgetreten.');
+ }
+ };
+
+ const handleConversationPress = (id: string) => {
+ // Navigiere zum Konversations-Screen mit der ID
+ router.push(`/conversation/${id}`);
+ };
+
+ // Löschen einer Konversation
+ const handleDeleteConversation = (id: string) => {
+ Alert.alert(
+ "Konversation löschen",
+ "Möchtest du diese Konversation wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
+ [
+ {
+ text: "Abbrechen",
+ style: "cancel"
+ },
+ {
+ text: "Löschen",
+ style: "destructive",
+ onPress: async () => {
+ try {
+ const success = await deleteConversation(id);
+ if (success) {
+ // Aus der lokalen Liste entfernen
+ setConversations(prev => prev.filter(conv => conv.id !== id));
+ Alert.alert("Erfolg", "Die Konversation wurde gelöscht.");
+ } else {
+ Alert.alert("Fehler", "Die Konversation konnte nicht gelöscht werden.");
+ }
+ } catch (error) {
+ console.error('Fehler beim Löschen der Konversation:', error);
+ Alert.alert("Fehler", "Die Konversation konnte nicht gelöscht werden.");
+ }
+ }
+ }
+ ]
+ );
+ };
+
+ // Archivieren einer Konversation
+ const handleArchiveConversation = async (id: string) => {
+ try {
+ const success = await archiveConversation(id);
+ if (success) {
+ // Aus der lokalen Liste entfernen
+ setConversations(prev => prev.filter(conv => conv.id !== id));
+ Alert.alert("Erfolg", "Die Konversation wurde archiviert.");
+ } else {
+ Alert.alert("Fehler", "Die Konversation konnte nicht archiviert werden.");
+ }
+ } catch (error) {
+ console.error('Fehler beim Archivieren der Konversation:', error);
+ Alert.alert("Fehler", "Die Konversation konnte nicht archiviert werden.");
+ }
+ };
+
+ // Zustandsverwaltung für die Optionsmenüs der Konversationselemente
+ const [expandedConversationId, setExpandedConversationId] = useState(null);
+
+ // Toggle-Funktion für das Optionsmenü
+ const toggleOptionsMenu = (id: string) => {
+ setExpandedConversationId(expandedConversationId === id ? null : id);
+ };
+
+ const renderConversationItem = ({ item }: { item: ConversationItem }) => {
+ const showOptions = expandedConversationId === item.id;
+
+ return (
+
+ [
+ styles.conversationItem,
+ hovered && { backgroundColor: colors.cardHover },
+ pressed && { opacity: 0.9 }
+ ]}
+ onPress={() => handleConversationPress(item.id)}
+ onLongPress={() => toggleOptionsMenu(item.id)}
+ >
+ {({ pressed, hovered }) => (
+ <>
+
+
+
+
+
+ {item.title}
+
+
+
+
+
+
+
+ {item.modelName}
+
+
+
+
+
+ {item.mode === 'frei' ? 'Frei' :
+ item.mode === 'geführt' ? 'Geführt' : 'Vorlage'}
+
+
+
+
+ {formatDate(item.timestamp)}
+
+
+
+
+ {item.lastMessage}
+
+
+
+ [
+ styles.optionsButton,
+ hovered && { backgroundColor: colors.menuItemHover },
+ pressed && { opacity: 0.7 }
+ ]}
+ onPress={() => toggleOptionsMenu(item.id)}
+ >
+ {({ pressed, hovered }) => (
+
+ )}
+
+ >
+ )}
+
+
+ {showOptions && (
+
+ [
+ styles.optionButton,
+ hovered && { backgroundColor: colors.menuItemHover },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={() => handleArchiveConversation(item.id)}
+ >
+ {({ pressed, hovered }) => (
+ <>
+
+ Archivieren
+ >
+ )}
+
+
+ [
+ styles.optionButton,
+ hovered && { backgroundColor: colors.dangerHover },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={() => handleDeleteConversation(item.id)}
+ >
+ {({ pressed, hovered }) => (
+ <>
+
+ Löschen
+ >
+ )}
+
+
+ )}
+
+ );
+ };
+
+ // Fokussiere das Eingabefeld, wenn der Benutzer auf "Neuen Chat starten" klickt
+ const handleFocusInput = useCallback(() => {
+ if (chatInputRef.current) {
+ chatInputRef.current.focus();
+ }
+ }, [chatInputRef]);
+
+ return (
+
+
+ {/* Permanenter Drawer links */}
+ {isDrawerOpen && (
+
+ setIsDrawerOpen(false)}
+ />
+
+ )}
+
+ {/* Hauptinhalt */}
+
+
+
+ [
+ styles.menuButton,
+ hovered && { backgroundColor: colors.menuItemHover },
+ pressed && { opacity: 0.7 }
+ ]}
+ onPress={() => setIsDrawerOpen(!isDrawerOpen)}
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+ >
+ {({ pressed, hovered }) => (
+
+ )}
+
+
+ Chats
+
+
+ {/* Space-Auswahl */}
+ {spaces.length > 0 && (
+
+
+ handleSpaceSelect(null)}
+ activeOpacity={0.7}
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+ >
+
+ Alle
+
+
+
+ {spaces.map(space => (
+ handleSpaceSelect(space.id)}
+ activeOpacity={0.7}
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+ >
+
+ {space.name}
+
+
+ ))}
+
+ router.push('/spaces')}
+ activeOpacity={0.7}
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+ >
+
+
+
+ Verwalten
+
+
+
+
+
+ )}
+
+ {/* Zentrierter ConversationStarter */}
+
+
+
+
+ {/* Konversationsliste unten */}
+
+
+
+ Letzte Konversationen
+
+ {conversations.length > 0 && (
+ [
+ styles.viewAllButton,
+ hovered && { backgroundColor: colors.buttonHover },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={() => router.push('/conversations')}
+ >
+ {({ pressed, hovered }) => (
+
+ Alle anzeigen
+
+ )}
+
+ )}
+
+
+ {isLoading ? (
+
+
+
+ Konversationen werden geladen...
+
+
+ ) : conversations.length > 0 ? (
+ item.id}
+ renderItem={renderConversationItem}
+ contentContainerStyle={styles.gridContent}
+ horizontal={true}
+ showsHorizontalScrollIndicator={false}
+ snapToAlignment="start"
+ decelerationRate="fast"
+ snapToInterval={396} // 380px Kartenbreite + 16px Abstand
+ />
+ ) : (
+
+
+
+ Keine Konversationen vorhanden
+
+
+ Stelle eine Frage im Eingabefeld oben
+
+
+ )}
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ mainLayout: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+ mainContainer: {
+ flex: 1,
+ alignItems: 'center',
+ },
+ drawerContainer: {
+ width: 260,
+ height: '100%',
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ bottom: 0,
+ zIndex: 10,
+ },
+ contentContainer: {
+ flex: 1,
+ maxWidth: 1200,
+ width: '100%',
+ },
+ header: {
+ paddingHorizontal: 20,
+ paddingTop: 16,
+ paddingBottom: 8,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ maxWidth: 800,
+ width: '100%',
+ alignSelf: 'center',
+ zIndex: 10, // Stelle sicher, dass der Header über allem anderen liegt
+ elevation: 10, // Für Android
+ },
+ menuButton: {
+ padding: 10,
+ marginRight: 8,
+ zIndex: 5,
+ borderRadius: 20,
+ width: 40,
+ height: 40,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ },
+ spaceSelector: {
+ paddingTop: 8,
+ paddingBottom: 12,
+ zIndex: 20, // Erhöht, um über anderen Elementen zu liegen
+ elevation: 20, // Für Android
+ position: 'relative', // Setzt einen neuen Stacking-Kontext
+ },
+ spacePills: {
+ paddingHorizontal: 16,
+ gap: 8,
+ },
+ spacePill: {
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ borderRadius: 16,
+ borderWidth: 1,
+ minWidth: 60,
+ minHeight: 32,
+ justifyContent: 'center',
+ alignItems: 'center',
+ zIndex: 25, // Noch höher als spaceSelector
+ elevation: 25, // Für Android
+ },
+ spacePillText: {
+ fontSize: 14,
+ fontWeight: '500',
+ },
+ spacePillAdd: {
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ borderRadius: 16,
+ borderWidth: 1,
+ borderStyle: 'dashed',
+ minWidth: 100,
+ minHeight: 32,
+ justifyContent: 'center',
+ alignItems: 'center',
+ zIndex: 25, // Gleich wie normaler spacePill
+ elevation: 25, // Für Android
+ },
+ spacePillAddContent: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ spacePillAddText: {
+ fontSize: 14,
+ fontWeight: '500',
+ marginLeft: 4,
+ },
+ centerContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ marginTop: 20, // Erhöht, um mehr Platz für Space-Pills zu lassen
+ zIndex: 10, // Zwischen Space-Selector und den Pills
+ },
+ bottomSection: {
+ flex: 0.4, // Nimmt 40% des verfügbaren Platzes ein
+ width: '100%',
+ },
+ gridContent: {
+ paddingLeft: 16,
+ paddingRight: 4, // Reduziertes Padding rechts, da die Karten marginRight haben
+ paddingBottom: 20,
+ paddingTop: 10,
+ },
+ conversationItemWrapper: {
+ borderRadius: 12,
+ overflow: 'hidden',
+ width: 380, // Breitere Karten
+ height: 180, // Feste Höhe für einheitlichere Darstellung
+ marginRight: 16, // Abstand zwischen den Karten
+ ...Platform.select({
+ ios: {
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ },
+ android: {
+ elevation: 3,
+ },
+ web: {
+ boxShadow: '0 2px 6px rgba(0, 0, 0, 0.1)',
+ },
+ }),
+ },
+ conversationItem: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ padding: 16,
+ },
+ conversationContent: {
+ flex: 1,
+ display: 'flex',
+ flexDirection: 'column',
+ height: '100%',
+ },
+ optionsContainer: {
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ paddingHorizontal: 16,
+ paddingBottom: 12,
+ paddingTop: 8,
+ },
+ optionButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ marginLeft: 8,
+ borderRadius: 6,
+ },
+ optionText: {
+ fontSize: 14,
+ marginLeft: 6,
+ fontWeight: '500',
+ },
+ conversationHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 6,
+ },
+ titleRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ },
+ titleIcon: {
+ marginRight: 8,
+ },
+ title: {
+ fontSize: 16,
+ fontWeight: '600',
+ flex: 1,
+ marginBottom: 2,
+ },
+ modelName: {
+ fontSize: 12,
+ fontWeight: '400',
+ },
+ badgeContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 10,
+ gap: 8,
+ flexWrap: 'wrap',
+ },
+ modelBadge: {
+ paddingHorizontal: 8,
+ paddingVertical: 3,
+ borderRadius: 12,
+ },
+ modelName: {
+ fontSize: 12,
+ fontWeight: '500',
+ },
+ modeBadge: {
+ paddingHorizontal: 8,
+ paddingVertical: 3,
+ borderRadius: 12,
+ },
+ timestamp: {
+ fontSize: 11,
+ marginLeft: 'auto', // Um es an den rechten Rand zu schieben
+ },
+ lastMessage: {
+ fontSize: 14,
+ marginBottom: 6,
+ lineHeight: 20,
+ marginTop: 4,
+ flex: 1, // Damit die Nachricht den verbleibenden Platz einnimmt
+ },
+ modeText: {
+ fontSize: 11,
+ fontWeight: '500',
+ },
+ optionsButton: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ // Container für den Ladezustand
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 32,
+ marginTop: -40,
+ },
+ loadingText: {
+ fontSize: 16,
+ marginTop: 16,
+ textAlign: 'center',
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 32,
+ marginTop: -80, // Nach oben verschieben, um Platz für das Eingabefeld zu machen
+ },
+ emptyText: {
+ fontSize: 18,
+ fontWeight: '600',
+ marginTop: 16,
+ textAlign: 'center',
+ },
+ emptySubtext: {
+ fontSize: 14,
+ textAlign: 'center',
+ marginTop: 8,
+ },
+ userContainer: {
+ flexDirection: 'column',
+ alignItems: 'flex-end',
+ },
+ userEmail: {
+ fontSize: 12,
+ marginBottom: 4,
+ },
+ logoutButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 10,
+ paddingVertical: 6,
+ borderRadius: 16,
+ },
+ logoutText: {
+ color: 'white',
+ fontSize: 12,
+ marginLeft: 4,
+ fontWeight: '500',
+ marginTop: 8,
+ textAlign: 'center',
+ },
+ buttonContainer: {
+ position: 'absolute',
+ bottom: 24,
+ right: 24,
+ },
+ sectionHeader: {
+ paddingHorizontal: 20,
+ paddingTop: 12,
+ paddingBottom: 4,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ maxWidth: 800,
+ alignSelf: 'center',
+ width: '100%',
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ },
+ viewAllButton: {
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ borderRadius: 16,
+ },
+ viewAllText: {
+ fontSize: 14,
+ fontWeight: '500',
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/app/model-selection.tsx b/chat/apps/mobile/app/model-selection.tsx
new file mode 100644
index 000000000..ffd5365ac
--- /dev/null
+++ b/chat/apps/mobile/app/model-selection.tsx
@@ -0,0 +1,178 @@
+import React, { useEffect, useState } from 'react';
+import { View, Text, StyleSheet, FlatList, SafeAreaView, TouchableOpacity } from 'react-native';
+import { useRouter, useLocalSearchParams } from 'expo-router';
+import { useTheme } from '@react-navigation/native';
+import { Ionicons } from '@expo/vector-icons';
+
+import ModelCard from '../components/ModelCard';
+import { getModels } from '../services/modelService';
+import { Model } from '../types';
+import { availableModels } from '../config/azure';
+
+export default function ModelSelectionScreen() {
+ const { colors } = useTheme();
+ const router = useRouter();
+ const params = useLocalSearchParams();
+ const initialMessage = params.initialMessage as string || '';
+ const [models, setModels] = useState(availableModels);
+ const [selectedModelId, setSelectedModelId] = useState(availableModels[0].id);
+ const [loading, setLoading] = useState(true);
+
+ // Extrahiere mögliche Space ID aus den Parametern
+ const spaceId = params.spaceId as string || null;
+
+ useEffect(() => {
+ // Lade Modelle vom Service
+ const loadModels = async () => {
+ try {
+ setLoading(true);
+ const modelsList = await getModels();
+ setModels(modelsList);
+
+ // Setze das erste Modell als Standard, wenn noch keins ausgewählt ist
+ if (!selectedModelId && modelsList.length > 0) {
+ setSelectedModelId(modelsList[0].id);
+ }
+ } catch (error) {
+ console.error('Fehler beim Laden der Modelle:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadModels();
+ }, []);
+
+ const handleSelectModel = (id: string) => {
+ setSelectedModelId(id);
+ };
+
+ const handleStart = () => {
+ // Navigiere zum Konversationsscreen mit ausgewähltem Modell und initialem Text
+ router.push({
+ pathname: '/conversation/new',
+ params: {
+ initialMessage,
+ modelId: selectedModelId,
+ mode: 'free',
+ ...(spaceId && { spaceId }) // Füge spaceId hinzu, wenn vorhanden
+ }
+ });
+ };
+
+ return (
+
+
+ router.back()}
+ style={styles.backButton}
+ >
+
+
+
+ Modell auswählen
+
+
+
+
+ Wähle das KI-Modell, mit dem du chatten möchtest
+
+
+ {loading ? (
+
+
+ Modelle werden geladen...
+
+
+ ) : (
+ item.id}
+ renderItem={({ item }) => (
+
+ )}
+ contentContainerStyle={styles.listContent}
+ />
+ )}
+
+
+
+ Konversation starten
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingTop: 16,
+ paddingBottom: 8,
+ },
+ backButton: {
+ marginRight: 16,
+ padding: 4,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ },
+ subtitle: {
+ paddingHorizontal: 16,
+ marginBottom: 16,
+ fontSize: 16,
+ },
+ listContent: {
+ paddingHorizontal: 16,
+ paddingBottom: 100,
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ loadingText: {
+ fontSize: 16,
+ },
+ footer: {
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ paddingHorizontal: 16,
+ paddingVertical: 16,
+ borderTopWidth: StyleSheet.hairlineWidth,
+ borderTopColor: 'rgba(0,0,0,0.1)',
+ backgroundColor: 'rgba(255,255,255,0.9)',
+ },
+ startButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 12,
+ paddingVertical: 16,
+ },
+ startButtonText: {
+ color: 'white',
+ fontSize: 16,
+ fontWeight: '600',
+ marginRight: 8,
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/app/profile.tsx b/chat/apps/mobile/app/profile.tsx
new file mode 100644
index 000000000..d994c5f21
--- /dev/null
+++ b/chat/apps/mobile/app/profile.tsx
@@ -0,0 +1,720 @@
+import React, { useState, useEffect } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, Alert, Image, ScrollView, ActivityIndicator, Platform } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { useAuth } from '../context/AuthProvider';
+import { useAppTheme } from '../theme/ThemeProvider';
+import { supabase } from '../utils/supabase';
+
+// Typendefinitionen für die Token-Nutzung
+type ModelUsage = {
+ model_id: string;
+ model_name: string;
+ total_prompt_tokens: number;
+ total_completion_tokens: number;
+ total_tokens: number;
+ total_cost: number;
+};
+
+type UsageByPeriod = {
+ time_period: string;
+ total_tokens: number;
+ total_cost: number;
+};
+
+type UsageSummary = {
+ totalCost: number;
+ totalTokens: number;
+ modelCount: number;
+ periodCount: number;
+};
+
+export default function ProfileScreen() {
+ const { colors } = useTheme();
+ const { isDarkMode, toggleTheme } = useAppTheme();
+ const router = useRouter();
+ const { user, signOut } = useAuth();
+
+ // Zustandsvariablen für Token-Nutzungsdaten
+ const [modelUsage, setModelUsage] = useState([]);
+ const [periodUsage, setPeriodUsage] = useState([]);
+ const [summary, setSummary] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [selectedPeriod, setSelectedPeriod] = useState<'day' | 'month' | 'year'>('month');
+
+ // Funktion zum Laden der Token-Nutzungsdaten
+ const loadUsageData = async () => {
+ if (!user) return;
+
+ setIsLoading(true);
+ try {
+ // Lade die Token-Nutzung nach Modell
+ const { data: modelData, error: modelError } = await supabase
+ .rpc('get_user_model_usage', { user_id: user.id });
+
+ if (modelError) {
+ console.error('Fehler beim Laden der Modellnutzung:', modelError);
+ } else if (modelData) {
+ setModelUsage(modelData as ModelUsage[]);
+ }
+
+ // Lade die Token-Nutzung nach Zeitraum
+ const { data: periodData, error: periodError } = await supabase
+ .rpc('get_user_usage_by_period', {
+ user_id: user.id,
+ period: selectedPeriod
+ });
+
+ if (periodError) {
+ console.error('Fehler beim Laden der Zeitraumnutzung:', periodError);
+ } else if (periodData) {
+ setPeriodUsage(periodData as UsageByPeriod[]);
+ }
+
+ // Berechne die Zusammenfassung
+ if (modelData) {
+ const totalCost = (modelData as ModelUsage[]).reduce((sum, model) => sum + model.total_cost, 0);
+ const totalTokens = (modelData as ModelUsage[]).reduce((sum, model) => sum + model.total_tokens, 0);
+
+ setSummary({
+ totalCost,
+ totalTokens,
+ modelCount: (modelData as ModelUsage[]).length,
+ periodCount: periodData ? (periodData as UsageByPeriod[]).length : 0
+ });
+ }
+ } catch (error) {
+ console.error('Fehler beim Laden der Nutzungsdaten:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Lade die Nutzungsdaten beim ersten Rendern und wenn sich der Zeitraum ändert
+ useEffect(() => {
+ if (user) {
+ loadUsageData();
+ }
+ }, [user, selectedPeriod]);
+
+ // Formatierungsfunktionen
+ const formatCost = (cost: number): string => {
+ return `$${cost.toFixed(4)}`;
+ };
+
+ const formatTokens = (tokens: number): string => {
+ if (tokens >= 1000000) {
+ return `${(tokens / 1000000).toFixed(2)}M`;
+ } else if (tokens >= 1000) {
+ return `${(tokens / 1000).toFixed(1)}K`;
+ } else {
+ return tokens.toString();
+ }
+ };
+
+ const handlePeriodChange = (period: 'day' | 'month' | 'year') => {
+ setSelectedPeriod(period);
+ };
+
+ const handleSignOut = async () => {
+ Alert.alert(
+ 'Abmelden',
+ 'Möchtest du dich wirklich abmelden?',
+ [
+ {
+ text: 'Abbrechen',
+ style: 'cancel',
+ },
+ {
+ text: 'Abmelden',
+ style: 'destructive',
+ onPress: async () => {
+ await signOut();
+ router.replace('/auth/login');
+ },
+ },
+ ],
+ );
+ };
+
+ return (
+
+
+ Profil
+
+
+
+
+
+ {user?.email?.charAt(0).toUpperCase() || 'U'}
+
+
+
+
+ {user?.email?.split('@')[0] || 'Benutzer'}
+
+
+ {user?.email || 'E-Mail nicht verfügbar'}
+
+
+
+
+ {/* Token-Nutzungsstatistiken */}
+
+ Token-Nutzung
+
+ {isLoading ? (
+
+
+
+ Lade Nutzungsdaten...
+
+
+ ) : summary ? (
+ <>
+ {/* Zusammenfassung der Nutzung */}
+
+
+
+
+ {formatTokens(summary.totalTokens)}
+
+
+ Tokens gesamt
+
+
+
+
+
+
+
+ ${summary.totalCost.toFixed(4)}
+
+
+ Kosten gesamt
+
+
+
+
+
+ {/* Zeitraumauswahl */}
+
+ handlePeriodChange('day')}
+ >
+
+ Tag
+
+
+
+ handlePeriodChange('month')}
+ >
+
+ Monat
+
+
+
+ handlePeriodChange('year')}
+ >
+
+ Jahr
+
+
+
+
+ {/* Modellnutzung */}
+ {modelUsage.length > 0 ? (
+
+
+ Modelle
+
+
+ {modelUsage.map((model, index) => (
+
+
+
+ {model.model_name}
+
+
+ ${model.total_cost.toFixed(4)}
+
+
+
+
+
+
+ {formatTokens(model.total_prompt_tokens)}
+
+
+ Prompt
+
+
+
+
+
+ {formatTokens(model.total_completion_tokens)}
+
+
+ Completion
+
+
+
+
+
+ {formatTokens(model.total_tokens)}
+
+
+ Gesamt
+
+
+
+
+ ))}
+
+ ) : (
+
+
+ Keine Modellnutzung vorhanden
+
+
+ )}
+
+ {/* Nutzung nach Zeitraum */}
+ {periodUsage.length > 0 ? (
+
+
+ Nutzung nach {
+ selectedPeriod === 'day' ? 'Tagen' :
+ selectedPeriod === 'month' ? 'Monaten' : 'Jahren'
+ }
+
+
+ {periodUsage.slice(0, 5).map((period, index) => (
+
+
+ {period.time_period}
+
+
+
+ {formatTokens(period.total_tokens)} Tokens
+
+
+ ${period.total_cost.toFixed(4)}
+
+
+
+ ))}
+
+ {periodUsage.length > 5 && (
+
+
+ Mehr anzeigen...
+
+
+ )}
+
+ ) : (
+
+
+ Keine Nutzungsdaten für diesen Zeitraum
+
+
+ )}
+ >
+ ) : (
+
+
+ Keine Nutzungsdaten verfügbar
+
+
+ )}
+
+
+
+ Einstellungen
+
+
+
+
+
+
+
+ Erscheinungsbild
+
+
+ {isDarkMode ? 'Dunkel' : 'Hell'}
+
+
+
+
+
+
+
+
+
+
+
+ Benachrichtigungen
+
+
+ Ein
+
+
+
+
+
+
+
+ Konto
+
+
+
+
+
+
+
+ Passwort ändern
+
+
+
+
+
+
+
+
+
+
+
+ Abmelden
+
+
+
+
+
+
+
+ Version 1.0.0
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 24,
+ },
+ header: {
+ marginTop: 20,
+ marginBottom: 30,
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ },
+ profileSection: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 30,
+ },
+ avatarContainer: {
+ width: 70,
+ height: 70,
+ borderRadius: 35,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 16,
+ },
+ avatarText: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ },
+ userInfo: {
+ flex: 1,
+ },
+ userName: {
+ fontSize: 20,
+ fontWeight: '600',
+ marginBottom: 4,
+ },
+ userEmail: {
+ fontSize: 14,
+ },
+ // Nutzungsstatistik-Stile
+ usageSection: {
+ marginBottom: 30,
+ },
+ loadingContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 20,
+ },
+ loadingText: {
+ marginTop: 10,
+ fontSize: 14,
+ },
+ usageSummaryCard: {
+ borderRadius: 12,
+ padding: 20,
+ marginBottom: 16,
+ borderWidth: 1,
+ ...Platform.select({
+ ios: {
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ },
+ android: {
+ elevation: 2,
+ },
+ web: {
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
+ },
+ }),
+ },
+ usageSummaryRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ alignItems: 'center',
+ },
+ usageSummaryItem: {
+ alignItems: 'center',
+ flex: 1,
+ },
+ usageSummaryDivider: {
+ width: 1,
+ height: 40,
+ backgroundColor: '#E5E5EA',
+ marginHorizontal: 10,
+ },
+ usageSummaryValue: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ marginBottom: 6,
+ },
+ usageSummaryLabel: {
+ fontSize: 14,
+ },
+ periodSelector: {
+ flexDirection: 'row',
+ marginBottom: 16,
+ justifyContent: 'center',
+ },
+ periodButton: {
+ paddingVertical: 8,
+ paddingHorizontal: 16,
+ borderRadius: 20,
+ marginHorizontal: 4,
+ borderWidth: 1,
+ borderColor: '#E5E5EA',
+ },
+ periodButtonText: {
+ fontSize: 14,
+ },
+ modelUsageContainer: {
+ marginBottom: 20,
+ },
+ usageSubtitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginBottom: 10,
+ },
+ modelUsageItem: {
+ borderRadius: 10,
+ padding: 12,
+ marginBottom: 10,
+ borderWidth: 1,
+ },
+ modelUsageHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 10,
+ },
+ modelName: {
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ modelCost: {
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ modelUsageDetails: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ tokenItem: {
+ alignItems: 'center',
+ flex: 1,
+ },
+ tokenCount: {
+ fontSize: 14,
+ fontWeight: '500',
+ },
+ tokenLabel: {
+ fontSize: 12,
+ marginTop: 4,
+ },
+ periodUsageContainer: {
+ marginBottom: 20,
+ },
+ periodUsageItem: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: 12,
+ borderRadius: 10,
+ marginBottom: 8,
+ borderWidth: 1,
+ },
+ periodLabel: {
+ fontSize: 15,
+ fontWeight: '500',
+ },
+ periodUsageContent: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ periodTokens: {
+ fontSize: 14,
+ marginRight: 10,
+ },
+ periodCost: {
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ viewMoreButton: {
+ padding: 10,
+ borderRadius: 8,
+ borderWidth: 1,
+ alignItems: 'center',
+ marginTop: 8,
+ },
+ viewMoreText: {
+ fontSize: 14,
+ fontWeight: '500',
+ },
+ emptyContainer: {
+ alignItems: 'center',
+ padding: 20,
+ },
+ emptyText: {
+ fontSize: 14,
+ },
+ // Bestehende Stile
+ settingsSection: {
+ marginBottom: 30,
+ },
+ accountSection: {
+ marginBottom: 30,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ marginBottom: 16,
+ },
+ settingItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ },
+ settingIconContainer: {
+ width: 40,
+ alignItems: 'center',
+ marginRight: 12,
+ },
+ settingContent: {
+ flex: 1,
+ },
+ settingTitle: {
+ fontSize: 16,
+ fontWeight: '500',
+ },
+ settingValue: {
+ fontSize: 14,
+ marginTop: 2,
+ },
+ appInfo: {
+ alignItems: 'center',
+ marginTop: 16,
+ paddingBottom: 20,
+ },
+ versionText: {
+ fontSize: 14,
+ },
+});
diff --git a/chat/apps/mobile/app/spaces/[id]/index.tsx b/chat/apps/mobile/app/spaces/[id]/index.tsx
new file mode 100644
index 000000000..65befa7b9
--- /dev/null
+++ b/chat/apps/mobile/app/spaces/[id]/index.tsx
@@ -0,0 +1,634 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, SafeAreaView, Alert, ActivityIndicator, FlatList, Pressable, Platform } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { useLocalSearchParams, useRouter, useFocusEffect } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { useAuth } from '../../../context/AuthProvider';
+import { getSpace, getSpaceMembers, getUserRoleInSpace, Space, SpaceMember } from '../../../services/space';
+import { getConversations, Conversation } from '../../../services/conversation';
+
+export default function SpaceDetailScreen() {
+ const { colors } = useTheme();
+ const router = useRouter();
+ const { id } = useLocalSearchParams();
+ const { user } = useAuth();
+
+ const [space, setSpace] = useState(null);
+ const [members, setMembers] = useState([]);
+ const [conversations, setConversations] = useState([]);
+ const [userRole, setUserRole] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [activeTab, setActiveTab] = useState<'conversations' | 'members'>('conversations');
+
+ // Lade Space-Details, Mitglieder und Konversationen
+ const loadSpaceData = useCallback(async () => {
+ if (!user || !id) return;
+
+ setIsLoading(true);
+ try {
+ // Parallele Anfragen für bessere Performance
+ const [spaceData, membersData, roleData] = await Promise.all([
+ getSpace(id as string),
+ getSpaceMembers(id as string),
+ getUserRoleInSpace(id as string, user.id)
+ ]);
+
+ if (spaceData) {
+ setSpace(spaceData);
+
+ // Lade Konversationen nur, wenn der Space gefunden wurde
+ const spaceConversations = await getConversations(user.id, spaceData.id);
+ setConversations(spaceConversations.filter(c => c.space_id === spaceData.id));
+ } else {
+ console.error('Space nicht gefunden');
+ Alert.alert('Fehler', 'Der Space konnte nicht gefunden werden.');
+ router.back();
+ return;
+ }
+
+ setMembers(membersData);
+ setUserRole(roleData);
+ } catch (error) {
+ console.error('Fehler beim Laden der Space-Daten:', error);
+ Alert.alert('Fehler', 'Die Space-Daten konnten nicht geladen werden.');
+ } finally {
+ setIsLoading(false);
+ }
+ }, [user, id, router]);
+
+ // Lade Daten beim ersten Rendern
+ useEffect(() => {
+ loadSpaceData();
+ }, [loadSpaceData]);
+
+ // Lade Daten erneut, wenn der Screen fokussiert wird
+ useFocusEffect(
+ useCallback(() => {
+ loadSpaceData();
+ return () => {};
+ }, [loadSpaceData])
+ );
+
+ // Zu einer Konversation navigieren
+ const handleConversationPress = (conversationId: string) => {
+ router.push(`/conversation/${conversationId}`);
+ };
+
+ // Neue Konversation in diesem Space starten
+ const handleNewConversation = () => {
+ if (!space) return;
+
+ router.push({
+ pathname: '/model-selection',
+ params: { spaceId: space.id }
+ });
+ };
+
+ // Neues Mitglied einladen
+ const handleInviteMember = () => {
+ if (!space) return;
+
+ router.push(`/spaces/${space.id}/invite`);
+ };
+
+ // Mitgliederliste rendern
+ const renderMemberItem = ({ item }: { item: SpaceMember }) => {
+ const isOwner = item.role === 'owner';
+
+ return (
+
+
+
+ {item.user_id.substring(0, 1).toUpperCase()}
+
+
+
+
+
+ {item.user_id.substring(0, 8)}...
+
+
+
+
+
+ {isOwner ? 'Besitzer' :
+ item.role === 'admin' ? 'Admin' :
+ item.role === 'member' ? 'Mitglied' : 'Zuschauer'}
+
+
+
+
+ {item.joined_at
+ ? `Beigetreten: ${new Date(item.joined_at).toLocaleDateString()}`
+ : item.invitation_status === 'pending'
+ ? 'Einladung ausstehend'
+ : 'Status: ' + item.invitation_status}
+
+
+
+
+ );
+ };
+
+ // Konversationsliste rendern
+ const renderConversationItem = ({ item }: { item: Conversation }) => {
+ return (
+ [
+ styles.conversationItem,
+ {
+ backgroundColor: colors.card,
+ borderColor: colors.border
+ },
+ hovered && { backgroundColor: colors.cardHover },
+ pressed && { opacity: 0.9 }
+ ]}
+ onPress={() => handleConversationPress(item.id)}
+ >
+ {({ pressed, hovered }) => (
+ <>
+
+
+
+
+
+
+ {item.title || 'Unbenannte Konversation'}
+
+
+
+ {new Date(item.updated_at).toLocaleString()}
+
+
+
+
+ >
+ )}
+
+ );
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
+ Space wird geladen...
+
+
+
+ );
+ }
+
+ if (!space) {
+ return (
+
+
+
+
+ Space nicht gefunden
+
+ router.push('/spaces')}
+ >
+ Zurück zu Spaces
+
+
+
+ );
+ }
+
+ return (
+
+
+ router.back()}
+ >
+
+
+
+ {space.name}
+
+
+
+ {/* Space-Info Card */}
+
+
+
+
+
+ {space.name}
+
+ {userRole === 'owner' ? 'Du bist Besitzer' :
+ userRole === 'admin' ? 'Du bist Admin' :
+ userRole === 'member' ? 'Du bist Mitglied' : 'Du bist Zuschauer'}
+
+
+
+
+ {(userRole === 'owner' || userRole === 'admin') && (
+ router.push(`/spaces/${space.id}/settings`)}
+ >
+
+
+ )}
+
+
+ {space.description && (
+
+ {space.description}
+
+ )}
+
+
+
+
+
+ {members.length} Mitglieder
+
+
+
+
+
+
+ Erstellt: {new Date(space.created_at).toLocaleDateString()}
+
+
+
+
+
+ {/* Tabs */}
+
+ setActiveTab('conversations')}
+ >
+
+ Konversationen
+
+
+
+ setActiveTab('members')}
+ >
+
+ Mitglieder
+
+
+
+
+ {/* Tab-Inhalte */}
+ {activeTab === 'conversations' ? (
+
+
+
+ Neue Konversation
+
+
+ {conversations.length > 0 ? (
+ item.id}
+ renderItem={renderConversationItem}
+ contentContainerStyle={styles.listContent}
+ />
+ ) : (
+
+
+
+ Keine Konversationen
+
+
+ Starte eine neue Konversation in diesem Space
+
+
+ )}
+
+ ) : (
+
+ {(userRole === 'owner' || userRole === 'admin') && (
+
+
+ Mitglied einladen
+
+ )}
+
+ {members.length > 0 ? (
+ item.id}
+ renderItem={renderMemberItem}
+ contentContainerStyle={styles.listContent}
+ />
+ ) : (
+
+
+
+ Keine Mitglieder
+
+
+ Lade Mitglieder zu diesem Space ein
+
+
+ )}
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+ backButton: {
+ padding: 8,
+ },
+ headerTitle: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginLeft: 8,
+ flex: 1,
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ loadingText: {
+ fontSize: 16,
+ marginTop: 16,
+ },
+ errorContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
+ },
+ errorText: {
+ fontSize: 18,
+ fontWeight: '600',
+ marginVertical: 16,
+ },
+ backToSpacesButton: {
+ paddingVertical: 10,
+ paddingHorizontal: 20,
+ borderRadius: 8,
+ marginTop: 20,
+ },
+ backToSpacesText: {
+ color: 'white',
+ fontSize: 16,
+ fontWeight: '500',
+ },
+ spaceInfoCard: {
+ margin: 16,
+ padding: 16,
+ borderRadius: 12,
+ borderWidth: 1,
+ ...Platform.select({
+ ios: {
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ },
+ android: {
+ elevation: 3,
+ },
+ web: {
+ boxShadow: '0 2px 6px rgba(0, 0, 0, 0.1)',
+ },
+ }),
+ },
+ spaceInfoHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ marginBottom: 12,
+ },
+ spaceInfoTitleRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ },
+ spaceInfoIcon: {
+ marginRight: 12,
+ },
+ spaceInfoTitleContainer: {
+ flex: 1,
+ },
+ spaceInfoTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ },
+ spaceInfoSubtitle: {
+ fontSize: 14,
+ marginTop: 2,
+ },
+ editButton: {
+ padding: 8,
+ borderRadius: 20,
+ width: 36,
+ height: 36,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ spaceInfoDescription: {
+ fontSize: 15,
+ lineHeight: 22,
+ marginBottom: 16,
+ },
+ spaceInfoDetails: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ },
+ spaceInfoDetail: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginRight: 16,
+ marginBottom: 4,
+ },
+ spaceInfoDetailText: {
+ fontSize: 13,
+ marginLeft: 6,
+ },
+ tabContainer: {
+ flexDirection: 'row',
+ borderBottomWidth: 1,
+ marginBottom: 16,
+ },
+ tabButton: {
+ flex: 1,
+ paddingVertical: 12,
+ alignItems: 'center',
+ borderBottomWidth: 0,
+ },
+ tabButtonText: {
+ fontSize: 16,
+ fontWeight: '500',
+ },
+ tabContent: {
+ flex: 1,
+ paddingHorizontal: 16,
+ },
+ newButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 12,
+ borderRadius: 10,
+ marginBottom: 16,
+ },
+ newButtonText: {
+ color: 'white',
+ fontSize: 16,
+ fontWeight: '600',
+ marginLeft: 8,
+ },
+ listContent: {
+ paddingBottom: 20,
+ },
+ conversationItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 16,
+ borderRadius: 10,
+ borderWidth: 1,
+ marginBottom: 12,
+ },
+ conversationIcon: {
+ marginRight: 12,
+ },
+ conversationContent: {
+ flex: 1,
+ },
+ conversationTitle: {
+ fontSize: 16,
+ fontWeight: '500',
+ marginBottom: 4,
+ },
+ conversationDate: {
+ fontSize: 13,
+ },
+ memberItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 12,
+ borderRadius: 10,
+ borderWidth: 1,
+ marginBottom: 10,
+ },
+ memberAvatar: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginRight: 12,
+ },
+ memberInitial: {
+ color: 'white',
+ fontSize: 18,
+ fontWeight: 'bold',
+ },
+ memberContent: {
+ flex: 1,
+ },
+ memberUserId: {
+ fontSize: 15,
+ fontWeight: '500',
+ marginBottom: 4,
+ },
+ memberMeta: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flexWrap: 'wrap',
+ },
+ roleBadge: {
+ paddingHorizontal: 8,
+ paddingVertical: 3,
+ borderRadius: 10,
+ marginRight: 8,
+ },
+ roleBadgeText: {
+ fontSize: 12,
+ fontWeight: '500',
+ },
+ joinedDate: {
+ fontSize: 12,
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingTop: 40,
+ },
+ emptyTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ marginTop: 16,
+ },
+ emptyText: {
+ fontSize: 14,
+ textAlign: 'center',
+ marginTop: 8,
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/app/spaces/index.tsx b/chat/apps/mobile/app/spaces/index.tsx
new file mode 100644
index 000000000..97f9df371
--- /dev/null
+++ b/chat/apps/mobile/app/spaces/index.tsx
@@ -0,0 +1,503 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { View, Text, StyleSheet, FlatList, TouchableOpacity, SafeAreaView, Alert, ActivityIndicator, Pressable, Platform } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { useRouter, useFocusEffect } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { useAuth } from '../../context/AuthProvider';
+import { getUserSpaces, Space, deleteSpace } from '../../services/space';
+
+export default function SpaceListScreen() {
+ const { colors } = useTheme();
+ const router = useRouter();
+ const { user } = useAuth();
+ const [spaces, setSpaces] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [expandedSpaceId, setExpandedSpaceId] = useState(null);
+
+ // Laden der Spaces beim ersten Rendern und wenn der Screen fokussiert wird
+ const loadSpaces = useCallback(async () => {
+ if (!user) return;
+
+ setIsLoading(true);
+ try {
+ console.log("Lade Spaces für User:", user.id);
+ const userSpaces = await getUserSpaces(user.id);
+ console.log(`${userSpaces.length} Spaces geladen`);
+ setSpaces(userSpaces);
+ } catch (error) {
+ console.error('Fehler beim Laden der Spaces:', error);
+ Alert.alert('Fehler', 'Die Spaces konnten nicht geladen werden.');
+ } finally {
+ setIsLoading(false);
+ }
+ }, [user]);
+
+ // Lade Spaces beim ersten Rendern
+ useEffect(() => {
+ loadSpaces();
+ }, [loadSpaces]);
+
+ // Lade Spaces erneut, wenn der Screen fokussiert wird
+ useFocusEffect(
+ useCallback(() => {
+ loadSpaces();
+ return () => {};
+ }, [loadSpaces])
+ );
+
+ // Erstellen eines neuen Spaces
+ const handleCreateSpace = () => {
+ router.push('/spaces/new');
+ };
+
+ // Zu einem Space navigieren
+ const handleSpacePress = (id: string) => {
+ router.push(`/spaces/${id}`);
+ };
+
+ // Toggle-Funktion für das Optionsmenü
+ const toggleOptionsMenu = (id: string) => {
+ setExpandedSpaceId(expandedSpaceId === id ? null : id);
+ };
+
+ // Einen Space verlassen
+ const handleLeaveSpace = async (id: string) => {
+ Alert.alert(
+ "Space verlassen",
+ "Möchtest du diesen Space wirklich verlassen?",
+ [
+ {
+ text: "Abbrechen",
+ style: "cancel"
+ },
+ {
+ text: "Verlassen",
+ style: "destructive",
+ onPress: async () => {
+ // Diese Funktion würde einen Benutzer aus einem Space entfernen
+ // TODO: removeMember(id, user.id); implementieren
+ Alert.alert("Info", "Diese Funktion ist noch nicht implementiert.");
+ }
+ }
+ ]
+ );
+ };
+
+ // Einen Space löschen (nur für Besitzer)
+ const handleDeleteSpace = async (id: string) => {
+ Alert.alert(
+ "Space löschen",
+ "Möchtest du diesen Space wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
+ [
+ {
+ text: "Abbrechen",
+ style: "cancel"
+ },
+ {
+ text: "Löschen",
+ style: "destructive",
+ onPress: async () => {
+ try {
+ const success = await deleteSpace(id);
+
+ if (success) {
+ // Aus der lokalen Liste entfernen
+ setSpaces(prev => prev.filter(space => space.id !== id));
+ Alert.alert("Erfolg", "Der Space wurde gelöscht.");
+ } else {
+ Alert.alert("Fehler", "Der Space konnte nicht gelöscht werden.");
+ }
+ } catch (error) {
+ console.error('Fehler beim Löschen des Spaces:', error);
+ Alert.alert("Fehler", "Der Space konnte nicht gelöscht werden.");
+ }
+ }
+ }
+ ]
+ );
+ };
+
+ const renderSpaceItem = ({ item }: { item: Space }) => {
+ const showOptions = expandedSpaceId === item.id;
+ const isOwner = item.owner_id === user?.id;
+
+ return (
+
+ [
+ styles.spaceItem,
+ hovered && { backgroundColor: colors.cardHover },
+ pressed && { opacity: 0.9 }
+ ]}
+ onPress={() => handleSpacePress(item.id)}
+ onLongPress={() => toggleOptionsMenu(item.id)}
+ >
+ {({ pressed, hovered }) => (
+ <>
+
+
+
+
+
+ {item.name}
+
+ {isOwner && (
+
+
+ Besitzer
+
+
+ )}
+
+
+
+ {item.description && (
+
+ {item.description}
+
+ )}
+
+
+
+ Erstellt: {new Date(item.created_at).toLocaleDateString()}
+
+
+
+
+ [
+ styles.optionsButton,
+ hovered && { backgroundColor: colors.menuItemHover },
+ pressed && { opacity: 0.7 }
+ ]}
+ onPress={() => toggleOptionsMenu(item.id)}
+ >
+ {({ pressed, hovered }) => (
+
+ )}
+
+ >
+ )}
+
+
+ {showOptions && (
+
+ {isOwner && (
+ [
+ styles.optionButton,
+ hovered && { backgroundColor: colors.menuItemHover },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={() => router.push(`/spaces/${item.id}/settings`)}
+ >
+ {({ pressed, hovered }) => (
+ <>
+
+ Einstellungen
+ >
+ )}
+
+ )}
+
+ [
+ styles.optionButton,
+ hovered && { backgroundColor: colors.menuItemHover },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={() => router.push(`/spaces/${item.id}/invite`)}
+ >
+ {({ pressed, hovered }) => (
+ <>
+
+ Einladen
+ >
+ )}
+
+
+ {isOwner ? (
+ [
+ styles.optionButton,
+ hovered && { backgroundColor: colors.dangerHover },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={() => handleDeleteSpace(item.id)}
+ >
+ {({ pressed, hovered }) => (
+ <>
+
+ Löschen
+ >
+ )}
+
+ ) : (
+ [
+ styles.optionButton,
+ hovered && { backgroundColor: colors.dangerHover },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={() => handleLeaveSpace(item.id)}
+ >
+ {({ pressed, hovered }) => (
+ <>
+
+ Verlassen
+ >
+ )}
+
+ )}
+
+ )}
+
+ );
+ };
+
+ return (
+
+
+ router.back()}
+ >
+
+
+ Spaces
+
+
+
+ {/* Create new space button */}
+
+
+ Neuen Space erstellen
+
+
+ {/* Space list */}
+ {isLoading ? (
+
+
+
+ Spaces werden geladen...
+
+
+ ) : spaces.length > 0 ? (
+ item.id}
+ renderItem={renderSpaceItem}
+ contentContainerStyle={styles.listContent}
+ />
+ ) : (
+
+
+
+ Keine Spaces gefunden
+
+
+ Erstelle einen neuen Space oder frage nach einer Einladung
+
+
+ )}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+ backButton: {
+ padding: 8,
+ },
+ headerTitle: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ marginLeft: 8,
+ },
+ contentContainer: {
+ flex: 1,
+ paddingHorizontal: 16,
+ },
+ createSpaceButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 12,
+ borderRadius: 12,
+ marginBottom: 16,
+ },
+ createSpaceText: {
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginLeft: 8,
+ },
+ listContent: {
+ paddingBottom: 16,
+ },
+ spaceItemWrapper: {
+ borderRadius: 12,
+ overflow: 'hidden',
+ marginBottom: 16,
+ ...Platform.select({
+ ios: {
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ },
+ android: {
+ elevation: 3,
+ },
+ web: {
+ boxShadow: '0 2px 6px rgba(0, 0, 0, 0.1)',
+ },
+ }),
+ },
+ spaceItem: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ padding: 16,
+ },
+ spaceContent: {
+ flex: 1,
+ },
+ spaceHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 6,
+ },
+ titleRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ },
+ titleIcon: {
+ marginRight: 8,
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ flex: 1,
+ },
+ ownerBadge: {
+ paddingHorizontal: 8,
+ paddingVertical: 3,
+ borderRadius: 12,
+ marginLeft: 8,
+ },
+ ownerBadgeText: {
+ fontSize: 12,
+ fontWeight: '500',
+ },
+ description: {
+ fontSize: 14,
+ marginBottom: 12,
+ lineHeight: 20,
+ },
+ statsContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'flex-end',
+ },
+ timestamp: {
+ fontSize: 12,
+ },
+ optionsButton: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ optionsContainer: {
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ paddingHorizontal: 16,
+ paddingBottom: 12,
+ paddingTop: 8,
+ },
+ optionButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ marginLeft: 8,
+ borderRadius: 6,
+ },
+ optionText: {
+ fontSize: 14,
+ marginLeft: 6,
+ fontWeight: '500',
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ loadingText: {
+ fontSize: 16,
+ marginTop: 16,
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 32,
+ },
+ emptyText: {
+ fontSize: 18,
+ fontWeight: '600',
+ marginTop: 16,
+ },
+ emptySubtext: {
+ fontSize: 14,
+ textAlign: 'center',
+ marginTop: 8,
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/app/spaces/new.tsx b/chat/apps/mobile/app/spaces/new.tsx
new file mode 100644
index 000000000..6707d7920
--- /dev/null
+++ b/chat/apps/mobile/app/spaces/new.tsx
@@ -0,0 +1,214 @@
+import React, { useState } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, TextInput, SafeAreaView, Alert, ActivityIndicator, ScrollView } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { useAuth } from '../../context/AuthProvider';
+import { createSpace } from '../../services/space';
+
+export default function NewSpaceScreen() {
+ const { colors } = useTheme();
+ const router = useRouter();
+ const { user } = useAuth();
+ const [name, setName] = useState('');
+ const [description, setDescription] = useState('');
+ const [isCreating, setIsCreating] = useState(false);
+
+ // Validieren der Eingaben
+ const isValid = name.trim().length > 0;
+
+ // Erstellen eines neuen Spaces
+ const handleCreateSpace = async () => {
+ if (!isValid || !user) return;
+
+ setIsCreating(true);
+ try {
+ const spaceId = await createSpace(user.id, name.trim(), description.trim() || undefined);
+
+ if (spaceId) {
+ // Navigation zum neuen Space
+ Alert.alert("Erfolg", "Space wurde erfolgreich erstellt.", [
+ {
+ text: "OK",
+ onPress: () => router.push(`/spaces/${spaceId}`)
+ }
+ ]);
+ } else {
+ Alert.alert("Fehler", "Der Space konnte nicht erstellt werden.");
+ }
+ } catch (error) {
+ console.error('Fehler beim Erstellen des Spaces:', error);
+ Alert.alert("Fehler", "Der Space konnte nicht erstellt werden.");
+ } finally {
+ setIsCreating(false);
+ }
+ };
+
+ return (
+
+
+ router.back()}
+ >
+
+
+ Neuen Space erstellen
+
+
+
+
+ Name *
+
+
+ Beschreibung
+
+
+
+
+
+
+
+ Spaces sind Bereiche zum Organisieren von Konversationen und können mit anderen Nutzern geteilt werden.
+
+
+
+
+
+
+
+ {isCreating ? (
+
+ ) : (
+ Space erstellen
+ )}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+ backButton: {
+ padding: 8,
+ },
+ headerTitle: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginLeft: 8,
+ },
+ contentContainer: {
+ flex: 1,
+ },
+ scrollContent: {
+ paddingHorizontal: 20,
+ paddingTop: 20,
+ paddingBottom: 40,
+ },
+ formSection: {
+ marginBottom: 30,
+ },
+ label: {
+ fontSize: 16,
+ fontWeight: '500',
+ marginBottom: 8,
+ },
+ input: {
+ borderWidth: 1,
+ borderRadius: 10,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ fontSize: 16,
+ },
+ textArea: {
+ borderWidth: 1,
+ borderRadius: 10,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ fontSize: 16,
+ minHeight: 120,
+ },
+ infoSection: {
+ marginBottom: 20,
+ },
+ infoItem: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ marginBottom: 12,
+ },
+ infoIcon: {
+ marginRight: 8,
+ marginTop: 2,
+ },
+ infoText: {
+ fontSize: 14,
+ flex: 1,
+ lineHeight: 20,
+ },
+ footer: {
+ borderTopWidth: 1,
+ paddingHorizontal: 20,
+ paddingVertical: 16,
+ },
+ createButton: {
+ paddingVertical: 14,
+ borderRadius: 10,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ createButtonText: {
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/app/templates.tsx b/chat/apps/mobile/app/templates.tsx
new file mode 100644
index 000000000..1a57bf0cc
--- /dev/null
+++ b/chat/apps/mobile/app/templates.tsx
@@ -0,0 +1,435 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ FlatList,
+ TouchableOpacity,
+ SafeAreaView,
+ Alert,
+ Modal,
+ ActivityIndicator
+} from 'react-native';
+import { useTheme, useFocusEffect } from '@react-navigation/native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { useAuth } from '../context/AuthProvider';
+import { useAppTheme } from '../theme/ThemeProvider';
+
+import TemplateCard from '../components/TemplateCard';
+import TemplateForm from '../components/TemplateForm';
+import CustomDrawer from '../components/CustomDrawer';
+import {
+ Template,
+ getTemplates,
+ createTemplate,
+ updateTemplate,
+ deleteTemplate,
+ setDefaultTemplate
+} from '../services/template';
+
+export default function TemplatesScreen() {
+ const { colors } = useTheme();
+ const router = useRouter();
+ const { user } = useAuth();
+ const { isDarkMode } = useAppTheme();
+
+ const [templates, setTemplates] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isFormModalVisible, setIsFormModalVisible] = useState(false);
+ const [selectedTemplate, setSelectedTemplate] = useState(null);
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false);
+
+ // Lade die Vorlagen
+ const loadTemplates = async () => {
+ if (!user) return;
+
+ setIsLoading(true);
+ try {
+ const userTemplates = await getTemplates(user.id);
+ setTemplates(userTemplates);
+ } catch (error) {
+ console.error('Fehler beim Laden der Vorlagen:', error);
+ Alert.alert('Fehler', 'Die Vorlagen konnten nicht geladen werden.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Lade Vorlagen beim ersten Laden und wenn der Benutzer sich ändert
+ useEffect(() => {
+ loadTemplates();
+ }, [user]);
+
+ // Lade Vorlagen erneut, wenn der Screen fokussiert wird
+ useFocusEffect(
+ useCallback(() => {
+ if (user) loadTemplates();
+ return () => {};
+ }, [user])
+ );
+
+ // Öffne das Formular zum Erstellen einer neuen Vorlage
+ const handleCreateTemplate = () => {
+ setSelectedTemplate(null);
+ setIsFormModalVisible(true);
+ };
+
+ // Öffne das Formular zum Bearbeiten einer Vorlage
+ const handleEditTemplate = (id: string) => {
+ const template = templates.find(t => t.id === id);
+ if (template) {
+ setSelectedTemplate(template);
+ setIsFormModalVisible(true);
+ }
+ };
+
+ // Lösche eine Vorlage nach Bestätigung
+ const handleDeleteTemplate = (id: string) => {
+ Alert.alert(
+ "Vorlage löschen",
+ "Möchtest du diese Vorlage wirklich löschen?",
+ [
+ {
+ text: "Abbrechen",
+ style: "cancel"
+ },
+ {
+ text: "Löschen",
+ style: "destructive",
+ onPress: async () => {
+ try {
+ const success = await deleteTemplate(id);
+ if (success) {
+ setTemplates(prev => prev.filter(t => t.id !== id));
+ } else {
+ Alert.alert("Fehler", "Die Vorlage konnte nicht gelöscht werden.");
+ }
+ } catch (error) {
+ console.error('Fehler beim Löschen der Vorlage:', error);
+ Alert.alert("Fehler", "Die Vorlage konnte nicht gelöscht werden.");
+ }
+ }
+ }
+ ]
+ );
+ };
+
+ // Setze eine Vorlage als Standard
+ const handleSetDefaultTemplate = async (id: string) => {
+ if (!user) return;
+
+ try {
+ const success = await setDefaultTemplate(id, user.id);
+ if (success) {
+ // Aktualisiere den lokalen Zustand, um die Änderungen anzuzeigen
+ setTemplates(prev =>
+ prev.map(t => ({
+ ...t,
+ is_default: t.id === id
+ }))
+ );
+ } else {
+ Alert.alert("Fehler", "Die Standardvorlage konnte nicht gesetzt werden.");
+ }
+ } catch (error) {
+ console.error('Fehler beim Setzen der Standardvorlage:', error);
+ Alert.alert("Fehler", "Die Standardvorlage konnte nicht gesetzt werden.");
+ }
+ };
+
+ // Speichert eine neue oder bearbeitete Vorlage
+ const handleSubmitTemplate = async (templateData: Partial) => {
+ if (!user) return;
+
+ try {
+ // Prüfe, ob wir eine bestehende Vorlage bearbeiten oder eine neue erstellen
+ if (templateData.id) {
+ // Aktualisiere eine bestehende Vorlage
+ const success = await updateTemplate(templateData.id, {
+ name: templateData.name,
+ description: templateData.description,
+ system_prompt: templateData.system_prompt,
+ initial_question: templateData.initial_question,
+ color: templateData.color,
+ model_id: templateData.model_id,
+ document_mode: templateData.document_mode
+ });
+
+ if (success) {
+ setTemplates(prev =>
+ prev.map(t =>
+ t.id === templateData.id
+ ? { ...t, ...templateData }
+ : t
+ )
+ );
+ } else {
+ Alert.alert("Fehler", "Die Vorlage konnte nicht aktualisiert werden.");
+ }
+ } else {
+ // Erstelle eine neue Vorlage
+ const newTemplate = await createTemplate({
+ user_id: user.id,
+ name: templateData.name!,
+ description: templateData.description,
+ system_prompt: templateData.system_prompt!,
+ initial_question: templateData.initial_question,
+ color: templateData.color!,
+ model_id: templateData.model_id,
+ is_default: false,
+ document_mode: templateData.document_mode || false,
+ });
+
+ if (newTemplate) {
+ setTemplates(prev => [...prev, newTemplate]);
+ } else {
+ Alert.alert("Fehler", "Die Vorlage konnte nicht erstellt werden.");
+ }
+ }
+
+ // Schließe das Modal
+ setIsFormModalVisible(false);
+ } catch (error) {
+ console.error('Fehler beim Speichern der Vorlage:', error);
+ Alert.alert("Fehler", "Die Vorlage konnte nicht gespeichert werden.");
+ }
+ };
+
+ // Starte einen neuen Chat mit einer Vorlage
+ const handleUseTemplate = (id: string) => {
+ const template = templates.find(t => t.id === id);
+ if (template) {
+ // Erstelle einen neuen Chat mit dieser Vorlage
+ router.push({
+ pathname: '/conversation/new',
+ params: {
+ templateId: template.id,
+ mode: 'template',
+ documentMode: template.document_mode ? 'true' : 'false'
+ }
+ });
+ }
+ };
+
+ return (
+
+
+ {/* Drawer / Seitenmenü */}
+ {isDrawerOpen && (
+
+ setIsDrawerOpen(false)}
+ />
+
+ )}
+
+ {/* Hauptinhalt */}
+
+
+
+ setIsDrawerOpen(!isDrawerOpen)}
+ >
+
+
+
+ Vorlagen
+
+
+
+ Neue Vorlage
+
+
+
+ {/* Beschreibung */}
+
+
+ Erstelle Vorlagen mit benutzerdefinierten System-Prompts für verschiedene KI-Verhaltensweisen.
+
+
+
+ {/* Vorlagenliste */}
+ {isLoading ? (
+
+
+
+ Vorlagen werden geladen...
+
+
+ ) : templates.length > 0 ? (
+ item.id}
+ renderItem={({ item }) => (
+
+ )}
+ contentContainerStyle={styles.listContent}
+ />
+ ) : (
+
+
+
+ Keine Vorlagen vorhanden
+
+
+ Erstelle deine erste Vorlage, um loszulegen
+
+
+ )}
+
+ {/* Modal für das Erstellen/Bearbeiten von Vorlagen */}
+ setIsFormModalVisible(false)}
+ >
+
+ setIsFormModalVisible(false)}
+ />
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ mainLayout: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+ mainContainer: {
+ flex: 1,
+ alignItems: 'center',
+ },
+ drawerContainer: {
+ width: 260,
+ height: '100%',
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ bottom: 0,
+ zIndex: 10,
+ },
+ contentContainer: {
+ flex: 1,
+ maxWidth: 1200,
+ width: '100%',
+ },
+ header: {
+ paddingHorizontal: 20,
+ paddingTop: 16,
+ paddingBottom: 8,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ maxWidth: 800,
+ width: '100%',
+ alignSelf: 'center',
+ },
+ menuButton: {
+ padding: 8,
+ marginRight: 8,
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ flex: 1,
+ marginLeft: 8,
+ },
+ addButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ borderRadius: 20,
+ },
+ addButtonText: {
+ color: 'white',
+ fontWeight: '500',
+ marginLeft: 4,
+ },
+ descriptionContainer: {
+ paddingHorizontal: 20,
+ marginBottom: 16,
+ maxWidth: 800,
+ width: '100%',
+ alignSelf: 'center',
+ },
+ description: {
+ fontSize: 14,
+ },
+ listContent: {
+ padding: 16,
+ paddingHorizontal: 20,
+ paddingBottom: 120,
+ maxWidth: 800,
+ width: '100%',
+ alignSelf: 'center',
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingTop: 40,
+ },
+ loadingText: {
+ marginTop: 16,
+ fontSize: 16,
+ },
+ emptyContainer: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 32,
+ paddingTop: 40,
+ height: 300,
+ },
+ emptyText: {
+ fontSize: 18,
+ fontWeight: '600',
+ marginTop: 16,
+ textAlign: 'center',
+ },
+ emptySubtext: {
+ fontSize: 14,
+ marginTop: 8,
+ textAlign: 'center',
+ },
+ modalContainer: {
+ flex: 1,
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/assets/adaptive-icon.png b/chat/apps/mobile/assets/adaptive-icon.png
new file mode 100644
index 000000000..03d6f6b6c
Binary files /dev/null and b/chat/apps/mobile/assets/adaptive-icon.png differ
diff --git a/chat/apps/mobile/assets/favicon.png b/chat/apps/mobile/assets/favicon.png
new file mode 100644
index 000000000..e75f697b1
Binary files /dev/null and b/chat/apps/mobile/assets/favicon.png differ
diff --git a/chat/apps/mobile/assets/icon.png b/chat/apps/mobile/assets/icon.png
new file mode 100644
index 000000000..a0b1526fc
Binary files /dev/null and b/chat/apps/mobile/assets/icon.png differ
diff --git a/chat/apps/mobile/assets/splash.png b/chat/apps/mobile/assets/splash.png
new file mode 100644
index 000000000..0e89705a9
Binary files /dev/null and b/chat/apps/mobile/assets/splash.png differ
diff --git a/chat/apps/mobile/babel.config.js b/chat/apps/mobile/babel.config.js
new file mode 100644
index 000000000..ef804fc20
--- /dev/null
+++ b/chat/apps/mobile/babel.config.js
@@ -0,0 +1,12 @@
+module.exports = function (api) {
+ api.cache(true);
+ const plugins = [];
+
+ plugins.push('react-native-reanimated/plugin');
+
+ return {
+ presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
+
+ plugins,
+ };
+};
diff --git a/chat/apps/mobile/cesconfig.json b/chat/apps/mobile/cesconfig.json
new file mode 100644
index 000000000..d916f5ca6
--- /dev/null
+++ b/chat/apps/mobile/cesconfig.json
@@ -0,0 +1,40 @@
+{
+ "cesVersion": "2.14.1",
+ "projectName": "chat",
+ "packages": [
+ {
+ "name": "expo-router",
+ "type": "navigation",
+ "options": {
+ "type": "drawer + tabs"
+ }
+ },
+ {
+ "name": "nativewind",
+ "type": "styling"
+ },
+ {
+ "name": "supabase",
+ "type": "authentication"
+ }
+ ],
+ "flags": {
+ "noGit": false,
+ "noInstall": false,
+ "overwrite": false,
+ "importAlias": true,
+ "packageManager": "npm",
+ "eas": true,
+ "publish": false
+ },
+ "packageManager": {
+ "type": "npm",
+ "version": "10.7.0"
+ },
+ "os": {
+ "type": "Darwin",
+ "platform": "darwin",
+ "arch": "arm64",
+ "kernelVersion": "24.1.0"
+ }
+}
diff --git a/chat/apps/mobile/components/Button.tsx b/chat/apps/mobile/components/Button.tsx
new file mode 100644
index 000000000..bbc381ebd
--- /dev/null
+++ b/chat/apps/mobile/components/Button.tsx
@@ -0,0 +1,22 @@
+import { forwardRef } from 'react';
+import { Text, TouchableOpacity, TouchableOpacityProps, View } from 'react-native';
+
+type ButtonProps = {
+ title: string;
+} & TouchableOpacityProps;
+
+export const Button = forwardRef(({ title, ...touchableProps }, ref) => {
+ return (
+
+ {title}
+
+ );
+});
+
+const styles = {
+ button: 'items-center bg-indigo-500 rounded-[28px] shadow-md p-4',
+ buttonText: 'text-white text-lg font-semibold text-center',
+};
diff --git a/chat/apps/mobile/components/ChatHeader.tsx b/chat/apps/mobile/components/ChatHeader.tsx
new file mode 100644
index 000000000..1540956df
--- /dev/null
+++ b/chat/apps/mobile/components/ChatHeader.tsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { Ionicons } from '@expo/vector-icons';
+import { useRouter } from 'expo-router';
+
+type ChatHeaderProps = {
+ title?: string;
+ modelName: string;
+ conversationMode: string;
+ onBackPress?: () => void;
+};
+
+export default function ChatHeader({
+ title,
+ modelName,
+ conversationMode,
+ onBackPress
+}: ChatHeaderProps) {
+ const { colors } = useTheme();
+ const router = useRouter();
+
+ const handleBackPress = () => {
+ if (onBackPress) {
+ onBackPress();
+ } else {
+ router.back();
+ }
+ };
+
+ return (
+
+
+
+ {title || 'Neuer Chat'}
+
+
+
+ {modelName}
+
+
+ {conversationMode === 'frei' ? 'Freier Modus' :
+ conversationMode === 'geführt' ? 'Geführter Modus' : 'Vorlagen-Modus'}
+
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: 'rgba(0,0,0,0.1)',
+ width: '100%',
+ maxWidth: 1200,
+ alignSelf: 'center',
+ },
+ backButton: {
+ padding: 4,
+ },
+ titleContainer: {
+ flex: 1,
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: '600',
+ },
+ subtitleContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: 4,
+ },
+ modelName: {
+ fontSize: 13,
+ fontWeight: '500',
+ },
+ modeText: {
+ fontSize: 13,
+ marginLeft: 8,
+ },
+ menuButton: {
+ padding: 4,
+ },
+});
diff --git a/chat/apps/mobile/components/ChatInput.tsx b/chat/apps/mobile/components/ChatInput.tsx
new file mode 100644
index 000000000..3cbda9194
--- /dev/null
+++ b/chat/apps/mobile/components/ChatInput.tsx
@@ -0,0 +1,122 @@
+import React from 'react';
+import { View, TextInput, TouchableOpacity, Text, ActivityIndicator } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import useChatInput from '../hooks/useChatInput';
+import ModelDropdown from './ModelDropdown';
+
+interface ChatInputProps {
+ onSend: (message: string) => void;
+ isLoading?: boolean;
+ placeholder?: string;
+ showModelSelection?: boolean;
+ selectedModelId?: string;
+ onSelectModel?: (id: string) => void;
+ showAttachments?: boolean;
+ showSearch?: boolean;
+}
+
+export default function ChatInput({
+ onSend,
+ isLoading = false,
+ placeholder = 'Nachricht eingeben...',
+ showModelSelection = false,
+ selectedModelId = '550e8400-e29b-41d4-a716-446655440000',
+ onSelectModel = () => {},
+ showAttachments = false,
+ showSearch = false,
+}: ChatInputProps) {
+ const {
+ text,
+ setText,
+ handleSend,
+ canSend,
+ isDarkMode,
+ } = useChatInput({
+ onSend,
+ isLoading,
+ placeholder,
+ });
+
+ return (
+
+
+ {showModelSelection && (
+
+
+ Modell:
+
+
+
+ )}
+
+
+
+
+ {(showAttachments || showSearch) && (
+
+ {showAttachments && (
+
+
+ Attach
+
+ )}
+
+ {showSearch && (
+
+
+ Search
+
+ )}
+
+ )}
+
+
+ {isLoading ? (
+
+
+
+
+ Wird gesendet...
+
+ ) : (
+ <>
+
+
+ Senden
+
+ >
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/chat/apps/mobile/components/ChatPromptInput.tsx b/chat/apps/mobile/components/ChatPromptInput.tsx
new file mode 100644
index 000000000..a828ec94a
--- /dev/null
+++ b/chat/apps/mobile/components/ChatPromptInput.tsx
@@ -0,0 +1,338 @@
+import React, { useState, forwardRef, useImperativeHandle, useRef, useEffect } from 'react';
+import { View, TextInput, TouchableOpacity, Text, ScrollView, StyleSheet, ActivityIndicator } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { Ionicons } from '@expo/vector-icons';
+import { useAppTheme } from '../theme/ThemeProvider';
+import ModelDropdown from './ModelDropdown';
+import { useRouter } from 'expo-router';
+import { createConversation, addMessage } from '../services/conversation';
+import { supabase } from '../utils/supabase';
+import { useAuth } from '../context/AuthProvider';
+import { Template, getTemplates } from '../services/template';
+
+type ConversationStarterProps = {
+ onSend?: (message: string) => void;
+ placeholder?: string;
+};
+
+// Definiere die Ref-Methoden, die von außen aufgerufen werden können
+export interface ConversationStarterRef {
+ focus: () => void;
+}
+
+const ConversationStarter = forwardRef(({ onSend, placeholder = 'Ask anything' }, ref) => {
+ const [text, setText] = useState('');
+ const [selectedModelId, setSelectedModelId] = useState('550e8400-e29b-41d4-a716-446655440000'); // Default to Azure OpenAI GPT-O3-Mini
+ const [isCreatingConversation, setIsCreatingConversation] = useState(false);
+ const [templates, setTemplates] = useState([]);
+ const [isLoadingTemplates, setIsLoadingTemplates] = useState(true);
+ const [selectedTemplate, setSelectedTemplate] = useState(null);
+ const { colors } = useTheme();
+ const { isDarkMode } = useAppTheme();
+ const router = useRouter();
+ const { user } = useAuth();
+ const inputRef = useRef(null);
+
+ // Expose methods via ref
+ useImperativeHandle(ref, () => ({
+ focus: () => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }
+ }));
+
+ // Laden der Vorlagen beim ersten Rendern
+ useEffect(() => {
+ const loadTemplates = async () => {
+ if (!user) return;
+
+ setIsLoadingTemplates(true);
+ try {
+ const userTemplates = await getTemplates(user.id);
+ setTemplates(userTemplates);
+ } catch (error) {
+ console.error('Fehler beim Laden der Vorlagen:', error);
+ } finally {
+ setIsLoadingTemplates(false);
+ }
+ };
+
+ loadTemplates();
+ }, [user]);
+
+ const handleSend = async () => {
+ if (text.trim()) {
+ console.log("handleSend wird aufgerufen mit Text:", text.trim());
+
+ // Prüfen ob onSend-Prop existiert, aber für jetzt ignorieren
+ if (onSend && false) { // Deaktiviert: wir wollen immer unseren eigenen Code ausführen
+ console.log("onSend-Prop gefunden, rufe diese auf");
+ onSend(text.trim());
+ setText('');
+ return;
+ }
+
+ // Andernfalls starte eine neue Konversation
+ try {
+ setIsCreatingConversation(true);
+ console.log("Starte Erstellung einer neuen Konversation...");
+
+ // Verwende den Benutzer aus dem Auth-Kontext
+ if (!user) {
+ console.error('Kein Benutzer angemeldet');
+ router.replace('/auth/login');
+ return;
+ }
+
+ console.log(`Chat starten mit Modell-ID: ${selectedModelId}`);
+
+ const trimmedText = text.trim();
+
+ // WICHTIG: Setze Text zurück, bevor wir navigieren (UI-Block vermeiden)
+ setText('');
+
+ const mode = selectedTemplate ? 'template' : 'free';
+ const templateId = selectedTemplate?.id;
+ const modelToUse = selectedTemplate?.model_id || selectedModelId;
+
+ // Versuche zwei verschiedene Methoden, damit eine davon funktioniert
+ try {
+ // 1. Methode: Mit Route-Parametern im Objekt
+ console.log(`Methode 1: Mit Parametern im Objekt (${mode}, ${templateId || 'keine Vorlage'})`);
+ router.push({
+ pathname: '/conversation/new',
+ params: {
+ initialMessage: trimmedText,
+ modelId: modelToUse,
+ mode: mode,
+ ...(templateId && { templateId })
+ }
+ });
+ } catch (routerError) {
+ console.error("Fehler bei Methode 1:", routerError);
+
+ // 2. Methode: Mit Query-String
+ console.log(`Methode 2: Mit Query-String`);
+ let queryParams = `?initialMessage=${encodeURIComponent(
+ trimmedText
+ )}&modelId=${encodeURIComponent(
+ modelToUse
+ )}&mode=${encodeURIComponent(mode)}`;
+
+ if (templateId) {
+ queryParams += `&templateId=${encodeURIComponent(templateId)}`;
+ }
+
+ router.push(`/conversation/new${queryParams}`);
+ }
+
+ // Zurücksetzen der ausgewählten Vorlage nach Navigation
+ setSelectedTemplate(null);
+
+ console.log(`Navigation zur Konversation ausgeführt`);
+ } catch (error) {
+ console.error('Fehler beim Erstellen der Konversation:', error);
+ alert(`Fehler: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`);
+ } finally {
+ setIsCreatingConversation(false);
+ }
+ } else {
+ console.log("Text ist leer, keine Aktion");
+ }
+ };
+
+ // Handler für das Auswählen einer Vorlage
+ const handleTemplateSelect = (template: Template) => {
+ // Wenn die Vorlage bereits ausgewählt ist, deaktivieren wir sie
+ if (selectedTemplate?.id === template.id) {
+ setSelectedTemplate(null);
+ // Zurücksetzen des Texts, wenn es die Vorschau war
+ if (text.startsWith('Frage: ')) {
+ setText('');
+ }
+ return;
+ }
+
+ // Sonst wählen wir die Vorlage aus
+ setSelectedTemplate(template);
+ setSelectedModelId(template.model_id || selectedModelId);
+
+ // Vorschau der initialen Frage im Eingabefeld anzeigen, wenn vorhanden
+ if (text.trim() === '') {
+ if (template.initial_question) {
+ setText(`Frage: ${template.initial_question}`);
+ }
+ }
+ };
+
+ return (
+
+
+
+ Modell:
+
+
+
+
+
+
+
+
+
+ Attach
+
+
+
+
+ Search
+
+
+
+ {
+ console.log("Senden-Button gedrückt");
+ handleSend();
+ }}
+ disabled={!text.trim() || isCreatingConversation}
+ activeOpacity={0.7}
+ >
+ {isCreatingConversation ? (
+
+
+
+
+ Wird erstellt...
+
+ ) : (
+ <>
+
+ Senden
+ >
+ )}
+
+
+
+
+
+
+
+ Vorlagen:
+
+
+ {isLoadingTemplates ? (
+
+
+
+ Laden...
+
+
+ ) : templates.length > 0 ? (
+ templates.map((template) => (
+ handleTemplateSelect(template)}
+ >
+
+
+ {template.name}
+
+ {selectedTemplate?.id === template.id && (
+
+ )}
+
+ ))
+ ) : (
+ router.push('/templates')}
+ >
+
+
+ Vorlage erstellen
+
+
+ )}
+ router.push('/templates')}
+ >
+
+
+ Verwalten
+
+
+
+
+
+
+ );
+});
+
+// Styles für Elemente, die nicht mit NativeWind gestylt werden können
+const styles = StyleSheet.create({
+ chipIcon: {
+ marginRight: 6,
+ },
+});
+
+export default ConversationStarter;
\ No newline at end of file
diff --git a/chat/apps/mobile/components/Container.tsx b/chat/apps/mobile/components/Container.tsx
new file mode 100644
index 000000000..384cb0985
--- /dev/null
+++ b/chat/apps/mobile/components/Container.tsx
@@ -0,0 +1,9 @@
+import { SafeAreaView } from 'react-native';
+
+export const Container = ({ children }: { children: React.ReactNode }) => {
+ return {children};
+};
+
+const styles = {
+ container: 'flex flex-1 m-6',
+};
diff --git a/chat/apps/mobile/components/ConversationStarter.tsx b/chat/apps/mobile/components/ConversationStarter.tsx
new file mode 100644
index 000000000..558672248
--- /dev/null
+++ b/chat/apps/mobile/components/ConversationStarter.tsx
@@ -0,0 +1,442 @@
+import React, { useState, forwardRef, useImperativeHandle, useRef, useEffect } from 'react';
+import { View, TextInput, TouchableOpacity, Text, ScrollView, StyleSheet, ActivityIndicator } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { Ionicons } from '@expo/vector-icons';
+import { useAppTheme } from '../theme/ThemeProvider';
+import ModelDropdown from './ModelDropdown';
+import { useRouter } from 'expo-router';
+import { createConversation, addMessage } from '../services/conversation';
+import { supabase } from '../utils/supabase';
+import { useAuth } from '../context/AuthProvider';
+import { Template, getTemplates } from '../services/template';
+import { Space, getUserSpaces } from '../services/space';
+
+type ConversationStarterProps = {
+ onSend?: (message: string) => void;
+ placeholder?: string;
+ spaceId?: string | null;
+};
+
+// Definiere die Ref-Methoden, die von außen aufgerufen werden können
+export interface ConversationStarterRef {
+ focus: () => void;
+}
+
+const ConversationStarter = forwardRef(({ onSend, placeholder = 'Was möchtest du wissen?', spaceId }, ref) => {
+ const [text, setText] = useState('');
+ const [selectedModelId, setSelectedModelId] = useState('550e8400-e29b-41d4-a716-446655440000'); // Default to Azure OpenAI GPT-O3-Mini
+ const [isCreatingConversation, setIsCreatingConversation] = useState(false);
+ const [templates, setTemplates] = useState([]);
+ const [isLoadingTemplates, setIsLoadingTemplates] = useState(true);
+ const [selectedTemplate, setSelectedTemplate] = useState(null);
+ const [documentMode, setDocumentMode] = useState(false);
+ const [currentSpace, setCurrentSpace] = useState(null);
+ const { colors } = useTheme();
+ const { isDarkMode } = useAppTheme();
+ const router = useRouter();
+ const { user } = useAuth();
+ const inputRef = useRef(null);
+
+ // Expose methods via ref
+ useImperativeHandle(ref, () => ({
+ focus: () => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }
+ }));
+
+ // Laden der Vorlagen und des aktuellen Space beim ersten Rendern
+ useEffect(() => {
+ const loadTemplates = async () => {
+ if (!user) return;
+
+ setIsLoadingTemplates(true);
+ try {
+ const userTemplates = await getTemplates(user.id);
+ setTemplates(userTemplates);
+ } catch (error) {
+ console.error('Fehler beim Laden der Vorlagen:', error);
+ } finally {
+ setIsLoadingTemplates(false);
+ }
+ };
+
+ loadTemplates();
+ }, [user]);
+
+ // Laden des Space-Namens, wenn eine spaceId vorhanden ist
+ useEffect(() => {
+ const loadSpace = async () => {
+ if (!spaceId) {
+ setCurrentSpace(null);
+ return;
+ }
+
+ try {
+ const space = await getSpace(spaceId);
+ setCurrentSpace(space);
+ } catch (error) {
+ console.error('Fehler beim Laden des Space:', error);
+ setCurrentSpace(null);
+ }
+ };
+
+ loadSpace();
+ }, [spaceId]);
+
+ // Tastatur-Event-Handler für Enter-Taste (besonders wichtig für Web)
+ const handleKeyPress = (e: any) => {
+ // Prüfen auf Enter ohne Shift für Submit
+ if (e.nativeEvent.key === 'Enter' && !e.nativeEvent.shiftKey) {
+ e.preventDefault(); // Verhindert Zeilenumbruch
+ handleSend();
+ }
+ };
+
+ const handleSend = async () => {
+ if (text.trim()) {
+ console.log("handleSend wird aufgerufen mit Text:", text.trim());
+
+ // Prüfen ob onSend-Prop existiert, aber für jetzt ignorieren
+ if (onSend && false) { // Deaktiviert: wir wollen immer unseren eigenen Code ausführen
+ console.log("onSend-Prop gefunden, rufe diese auf");
+ onSend(text.trim());
+ setText('');
+ return;
+ }
+
+ // Andernfalls starte eine neue Konversation
+ try {
+ setIsCreatingConversation(true);
+ console.log("Starte Erstellung einer neuen Konversation...");
+
+ // Verwende den Benutzer aus dem Auth-Kontext
+ if (!user) {
+ console.error('Kein Benutzer angemeldet');
+ router.replace('/auth/login');
+ return;
+ }
+
+ console.log(`Chat starten mit Modell-ID: ${selectedModelId}`);
+
+ const trimmedText = text.trim();
+
+ // WICHTIG: Setze Text zurück, bevor wir navigieren (UI-Block vermeiden)
+ setText('');
+
+ const mode = selectedTemplate ? 'template' : 'free';
+ const templateId = selectedTemplate?.id;
+ const modelToUse = selectedTemplate?.model_id || selectedModelId;
+
+ // Versuche zwei verschiedene Methoden, damit eine davon funktioniert
+ try {
+ // 1. Methode: Mit Route-Parametern im Objekt
+ console.log(`Methode 1: Mit Parametern im Objekt (${mode}, ${templateId || 'keine Vorlage'}, documentMode: ${documentMode}, spaceId: ${spaceId || 'keiner'})`);
+ router.push({
+ pathname: '/conversation/new',
+ params: {
+ initialMessage: trimmedText,
+ modelId: modelToUse,
+ mode: mode,
+ documentMode: documentMode ? 'true' : 'false',
+ ...(templateId && { templateId }),
+ ...(spaceId && { spaceId })
+ }
+ });
+ } catch (routerError) {
+ console.error("Fehler bei Methode 1:", routerError);
+
+ // 2. Methode: Mit Query-String
+ console.log(`Methode 2: Mit Query-String`);
+ let queryParams = `?initialMessage=${encodeURIComponent(
+ trimmedText
+ )}&modelId=${encodeURIComponent(
+ modelToUse
+ )}&mode=${encodeURIComponent(mode)}&documentMode=${encodeURIComponent(documentMode ? 'true' : 'false')}`;
+
+ if (templateId) {
+ queryParams += `&templateId=${encodeURIComponent(templateId)}`;
+ }
+
+ if (spaceId) {
+ queryParams += `&spaceId=${encodeURIComponent(spaceId)}`;
+ }
+
+ router.push(`/conversation/new${queryParams}`);
+ }
+
+ // Zurücksetzen der ausgewählten Vorlage nach Navigation
+ setSelectedTemplate(null);
+
+ console.log(`Navigation zur Konversation ausgeführt`);
+ } catch (error) {
+ console.error('Fehler beim Erstellen der Konversation:', error);
+ alert(`Fehler: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`);
+ } finally {
+ setIsCreatingConversation(false);
+ }
+ } else {
+ console.log("Text ist leer, keine Aktion");
+ }
+ };
+
+ // Handler für das Auswählen einer Vorlage
+ const handleTemplateSelect = (template: Template) => {
+ // Wenn die Vorlage bereits ausgewählt ist, deaktivieren wir sie
+ if (selectedTemplate?.id === template.id) {
+ setSelectedTemplate(null);
+ // Auch den Dokumentmodus zurücksetzen
+ setDocumentMode(false);
+ } else {
+ // Sonst wählen wir die Vorlage aus
+ setSelectedTemplate(template);
+
+ // Modell automatisch auswählen, wenn die Vorlage eines definiert
+ if (template.model_id) {
+ setSelectedModelId(template.model_id);
+ }
+
+ // Dokumentmodus automatisch übernehmen, wenn die Vorlage ihn aktiviert hat
+ setDocumentMode(template.document_mode || false);
+ console.log(`Template ${template.name} ausgewählt, Dokumentmodus: ${template.document_mode}`);
+ }
+
+ // Nach der Auswahl/Abwahl einer Vorlage das Eingabefeld fokussieren
+ // Kurze Verzögerung, um UI-Updates abzuschließen
+ setTimeout(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, 50);
+ };
+
+ return (
+
+ {/* Container für den Titel mit fester Höhe - verhindert Layout-Verschiebung */}
+
+ {selectedTemplate && (
+
+ {selectedTemplate.name}
+
+ )}
+
+ {currentSpace && (
+
+
+
+ Space: {currentSpace.name}
+
+
+ )}
+
+
+
+ {
+ if (text.trim()) {
+ handleSend();
+ }
+ }}
+ blurOnSubmit={false}
+ onKeyPress={handleKeyPress}
+ />
+
+
+
+ setDocumentMode(!documentMode)}
+ >
+
+
+ Dokument
+
+ {documentMode && (
+
+ )}
+
+
+
+
+ Attach
+
+
+
+
+ Search
+
+
+
+
+
+
+
+ {
+ console.log("Senden-Button gedrückt");
+ handleSend();
+ }}
+ disabled={!text.trim() || isCreatingConversation}
+ activeOpacity={0.7}
+ >
+ {isCreatingConversation ? (
+
+
+
+
+ Wird erstellt...
+
+ ) : (
+ <>
+
+ Senden
+ >
+ )}
+
+
+
+
+
+
+
+ Vorlagen:
+
+
+ {isLoadingTemplates ? (
+
+
+
+ Laden...
+
+
+ ) : templates.length > 0 ? (
+ templates.map((template) => (
+ handleTemplateSelect(template)}
+ >
+
+
+ {template.name}
+
+ {selectedTemplate?.id === template.id && (
+
+ )}
+
+ ))
+ ) : (
+ router.push('/templates')}
+ >
+
+
+ Vorlage erstellen
+
+
+ )}
+ router.push('/templates')}
+ >
+
+
+ Verwalten
+
+
+
+
+
+
+ );
+});
+
+// Styles für Elemente, die nicht mit NativeWind gestylt werden können
+const styles = StyleSheet.create({
+ chipIcon: {
+ marginRight: 6,
+ },
+});
+
+export default ConversationStarter;
\ No newline at end of file
diff --git a/chat/apps/mobile/components/CustomDrawer.tsx b/chat/apps/mobile/components/CustomDrawer.tsx
new file mode 100644
index 000000000..53a49b929
--- /dev/null
+++ b/chat/apps/mobile/components/CustomDrawer.tsx
@@ -0,0 +1,490 @@
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ Pressable,
+ ScrollView,
+ Dimensions,
+ StatusBar,
+ ActivityIndicator,
+ SafeAreaView,
+ Platform
+} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { useRouter } from 'expo-router';
+import { useTheme } from '@react-navigation/native';
+import { useAppTheme } from '../theme/ThemeProvider';
+import { useAuth } from '../context/AuthProvider';
+import { getConversations } from '../services/conversation';
+
+const DRAWER_WIDTH = 260; // Breite des Drawer-Menüs
+
+interface CustomDrawerProps {
+ isVisible: boolean;
+ focusInputOnHomeNavigate?: () => void;
+ onClose?: () => void;
+}
+
+export default function CustomDrawer({
+ isVisible,
+ focusInputOnHomeNavigate,
+ onClose
+}: CustomDrawerProps) {
+ const { colors } = useTheme();
+ const { isDarkMode } = useAppTheme();
+ const router = useRouter();
+ const { user, signOut } = useAuth();
+
+ const [recentChats, setRecentChats] = useState<{id: string, title: string}[]>([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ // Lade die letzten Chats
+ useEffect(() => {
+ const loadRecentChats = async () => {
+ if (!user || !isVisible) return;
+
+ setIsLoading(true);
+ try {
+ const conversations = await getConversations(user.id);
+
+ // Nimm nur die letzten 10 Konversationen
+ const recentOnes = conversations.slice(0, 10).map(conv => ({
+ id: conv.id,
+ title: conv.title || 'Unbenannte Konversation'
+ }));
+
+ setRecentChats(recentOnes);
+ } catch (error) {
+ console.error('Fehler beim Laden der letzten Chats:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ if (isVisible) {
+ loadRecentChats();
+ }
+ }, [user, isVisible]);
+
+ // Navigation zum Home-Screen (mit Input-Fokus)
+ const navigateToHome = () => {
+ router.push('/');
+ if (focusInputOnHomeNavigate) {
+ // Verzögerung, um sicherzustellen, dass der Bildschirm geladen ist
+ setTimeout(() => {
+ focusInputOnHomeNavigate();
+ }, 100);
+ }
+ };
+
+ // Navigation zu einer Konversation
+ const navigateToConversation = (id: string) => {
+ router.push(`/conversation/${id}`);
+ };
+
+ // Navigation zur Archiv-Seite
+ const navigateToArchive = () => {
+ router.push('/archive');
+ };
+
+ // Navigation zur Vorlagen-Seite
+ const navigateToTemplates = () => {
+ router.push('/templates');
+ };
+
+ // Navigation zur Dokumente-Seite
+ const navigateToDocuments = () => {
+ router.push('/documents');
+ };
+
+ // Navigation zur Profilseite
+ const navigateToProfile = () => {
+ router.push('/profile');
+ };
+
+ // Styling für das Drawer-Menü
+ const bgColor = isDarkMode ? '#1C1C1E' : '#FFFFFF';
+ const textColor = isDarkMode ? '#FFFFFF' : '#000000';
+ const separatorColor = isDarkMode ? '#38383A' : '#E5E5EA';
+ const activeColor = '#0A84FF';
+
+ // Wenn der Drawer nicht sichtbar sein soll, gib nichts zurück
+ if (!isVisible) {
+ return null;
+ }
+
+ return (
+
+ {/* Drawer-Header */}
+
+
+ Menu
+
+ [
+ styles.iconButton,
+ hovered && { backgroundColor: colors.menuItemHover }
+ ]}
+ >
+ {({ pressed, hovered }) => (
+
+ )}
+
+
+
+ {/* Hauptaktionen */}
+
+ [
+ styles.mainActionButton,
+ { backgroundColor: activeColor },
+ pressed && { opacity: 0.85 }
+ ]}
+ onPress={navigateToHome}
+ >
+
+ Neuen Chat starten
+
+
+ [
+ styles.mainActionButton,
+ {
+ backgroundColor: hovered ? colors.buttonHover : 'transparent',
+ borderWidth: 1,
+ borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
+ marginTop: 8
+ },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={navigateToArchive}
+ >
+
+ Archiv ansehen
+
+
+ [
+ styles.mainActionButton,
+ {
+ backgroundColor: hovered ? colors.buttonHover : 'transparent',
+ borderWidth: 1,
+ borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
+ marginTop: 8
+ },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={() => router.push('/conversations')}
+ >
+
+ Konversationen
+
+
+ [
+ styles.mainActionButton,
+ {
+ backgroundColor: hovered ? colors.buttonHover : 'transparent',
+ borderWidth: 1,
+ borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
+ marginTop: 8
+ },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={navigateToDocuments}
+ >
+
+ Dokumente ansehen
+
+
+ [
+ styles.mainActionButton,
+ {
+ backgroundColor: hovered ? colors.buttonHover : 'transparent',
+ borderWidth: 1,
+ borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
+ marginTop: 8
+ },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={navigateToTemplates}
+ >
+
+ Vorlagen verwalten
+
+
+ [
+ styles.mainActionButton,
+ {
+ backgroundColor: hovered ? colors.buttonHover : 'transparent',
+ borderWidth: 1,
+ borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
+ marginTop: 8
+ },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={() => router.push('/spaces')}
+ >
+
+ Spaces
+
+
+ [
+ styles.mainActionButton,
+ {
+ backgroundColor: hovered ? colors.buttonHover : 'transparent',
+ borderWidth: 1,
+ borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
+ marginTop: 8
+ },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={navigateToProfile}
+ >
+
+ Profil & Statistiken
+
+
+
+ {/* Trennlinie */}
+
+
+ {/* Letzte Chats */}
+
+
+ Letzte Chats
+
+
+
+ {/* Liste der letzten Chats */}
+ {isLoading ? (
+
+
+
+ Chats werden geladen...
+
+
+ ) : (
+
+ {recentChats.length > 0 ? (
+ recentChats.map((chat) => (
+ [
+ styles.chatItem,
+ hovered && { backgroundColor: colors.menuItemHover },
+ pressed && { opacity: 0.7 }
+ ]}
+ onPress={() => navigateToConversation(chat.id)}
+ >
+ {({ pressed, hovered }) => (
+ <>
+
+
+ {chat.title}
+
+ >
+ )}
+
+ ))
+ ) : (
+
+
+ Keine Chats vorhanden
+
+
+ )}
+
+ )}
+
+ {/* Benutzerinformationen und Logout-Button */}
+
+
+
+ {user && (
+
+
+
+ {user.email}
+
+
+ )}
+ [
+ styles.logoutButton,
+ { borderColor: separatorColor },
+ hovered && { backgroundColor: colors.dangerHover },
+ pressed && { opacity: 0.8 }
+ ]}
+ onPress={() => {
+ signOut().then(() => router.replace('/auth/login'));
+ }}
+ >
+ {({ pressed, hovered }) => (
+ <>
+
+
+ Abmelden
+
+ >
+ )}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ drawer: {
+ height: '100%',
+ elevation: 5,
+ shadowColor: '#000',
+ shadowOffset: { width: 2, height: 0 },
+ shadowOpacity: 0.3,
+ shadowRadius: 4,
+ },
+ drawerHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ paddingVertical: 16,
+ },
+ drawerTitle: {
+ fontSize: 22,
+ fontWeight: 'bold',
+ },
+ iconButton: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ mainActions: {
+ paddingHorizontal: 20,
+ paddingVertical: 16,
+ },
+ mainActionButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 12,
+ borderRadius: 8,
+ },
+ mainActionText: {
+ color: 'white',
+ fontSize: 16,
+ fontWeight: '500',
+ marginLeft: 8,
+ },
+ separator: {
+ height: 1,
+ marginVertical: 8,
+ },
+ recentChatsHeader: {
+ paddingHorizontal: 20,
+ paddingVertical: 12,
+ },
+ recentChatsTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ recentChatsList: {
+ flex: 1,
+ },
+ chatItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ paddingVertical: 12,
+ borderRadius: 8,
+ marginHorizontal: 8,
+ },
+ chatIcon: {
+ marginRight: 12,
+ },
+ chatTitle: {
+ fontSize: 15,
+ flex: 1,
+ },
+ loadingContainer: {
+ padding: 20,
+ alignItems: 'center',
+ },
+ loadingText: {
+ marginTop: 8,
+ fontSize: 14,
+ },
+ emptyChatsContainer: {
+ padding: 20,
+ alignItems: 'center',
+ },
+ emptyChatsText: {
+ fontSize: 14,
+ },
+ userSection: {
+ paddingHorizontal: 20,
+ paddingVertical: 16,
+ marginTop: 'auto',
+ },
+ userContainer: {
+ marginTop: 10,
+ },
+ userInfo: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 12,
+ },
+ userEmail: {
+ fontSize: 14,
+ marginLeft: 8,
+ },
+ logoutButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 10,
+ borderRadius: 8,
+ borderWidth: 1,
+ marginTop: 4,
+ },
+ logoutText: {
+ fontSize: 15,
+ fontWeight: '500',
+ marginLeft: 8,
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/components/DocumentPanel.tsx b/chat/apps/mobile/components/DocumentPanel.tsx
new file mode 100644
index 000000000..feeaad10a
--- /dev/null
+++ b/chat/apps/mobile/components/DocumentPanel.tsx
@@ -0,0 +1,385 @@
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ Text,
+ TextInput,
+ StyleSheet,
+ TouchableOpacity,
+ ScrollView,
+ ActivityIndicator,
+ useWindowDimensions,
+ Platform,
+ Alert,
+} from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { Ionicons } from '@expo/vector-icons';
+import { Document } from '../services/document';
+import Markdown from 'react-native-markdown-display';
+
+interface DocumentPanelProps {
+ document: Document | null;
+ isLoading?: boolean;
+ versionCount: number;
+ onSave?: (content: string) => void;
+ onShowVersions?: () => void;
+ onNextVersion?: () => void;
+ onPreviousVersion?: () => void;
+ onDeleteVersion?: (document: Document) => void;
+}
+
+// Hilfsfunktion, um zu prüfen, ob der Dark Mode aktiv ist
+const isDarkMode = (colors: any) => {
+ return colors.background === '#000' ||
+ colors.background === '#121212' ||
+ colors.background.includes('rgba(0,0,0') ||
+ colors.text === '#fff' ||
+ colors.text === '#ffffff';
+};
+
+export default function DocumentPanel({
+ document,
+ isLoading = false,
+ versionCount,
+ onSave,
+ onShowVersions,
+ onNextVersion,
+ onPreviousVersion,
+ onDeleteVersion
+}: DocumentPanelProps) {
+ const { colors } = useTheme();
+ const [content, setContent] = useState(document?.content || '');
+ const [editing, setEditing] = useState(false);
+ const { width } = useWindowDimensions();
+
+ // Aktualisiere den Content, wenn sich das Dokument ändert
+ useEffect(() => {
+ if (document) {
+ setContent(document.content);
+ }
+ }, [document]);
+
+ const handleEdit = () => {
+ setEditing(true);
+ };
+
+ const handleCancel = () => {
+ setContent(document?.content || '');
+ setEditing(false);
+ };
+
+ const handleSave = () => {
+ if (onSave) {
+ onSave(content);
+ }
+ setEditing(false);
+ };
+
+ const renderVersionControls = () => {
+ // Aktuelle Version und Versionszählung
+ const currentVersion = document?.version || 1;
+ const hasMultipleVersions = versionCount > 1;
+ const canGoBack = currentVersion > 1;
+ const canGoForward = currentVersion < versionCount;
+
+ return (
+
+ {/* Pfeil zurück */}
+
+
+
+
+ {/* Version Badge */}
+
+ v{currentVersion}
+ {hasMultipleVersions && (
+ {versionCount}
+ )}
+
+
+ {/* Pfeil vorwärts */}
+
+
+
+
+ );
+ };
+
+ if (isLoading) {
+ return (
+
+
+ Dokument
+
+
+
+
+ Dokument wird geladen...
+
+
+
+ );
+ }
+
+ return (
+
+
+ Dokument
+ {renderVersionControls()}
+
+ {editing ? (
+ <>
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+ {document && onDeleteVersion && versionCount > 1 && (
+ {
+ if (document) {
+ console.log('Löschen-Button in DocumentPanel gedrückt für Version:', document.version);
+
+ Alert.alert(
+ "Version löschen",
+ `Möchtest du die Version ${document.version} wirklich löschen?`,
+ [
+ {
+ text: "Abbrechen",
+ style: "cancel"
+ },
+ {
+ text: "Löschen",
+ style: "destructive",
+ onPress: () => {
+ console.log('Löschvorgang bestätigt für Version:', document.version);
+ if (onDeleteVersion) {
+ onDeleteVersion(document);
+ } else {
+ console.error('onDeleteVersion Funktion ist nicht definiert');
+ }
+ }
+ }
+ ]
+ );
+ }
+ }}
+ >
+
+ Löschen
+
+ )}
+
+
+
+ >
+ )}
+
+
+ {editing ? (
+
+ ) : (
+
+ {document?.content ? (
+
+ {document.content}
+
+ ) : (
+
+ Noch kein Dokument erstellt.
+
+ )}
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ borderRadius: 12,
+ overflow: 'hidden',
+ marginHorizontal: 16,
+ marginBottom: 16,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(0,0,0,0.1)',
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: '600',
+ flex: 1,
+ },
+ versionControls: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginRight: 8,
+ },
+ versionArrow: {
+ width: 24,
+ height: 24,
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderRadius: 12,
+ backgroundColor: 'rgba(0,0,0,0.05)',
+ },
+ versionArrowDisabled: {
+ backgroundColor: 'rgba(0,0,0,0.02)',
+ },
+ versionBadge: {
+ backgroundColor: 'rgba(0,0,0,0.1)',
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ borderRadius: 12,
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginHorizontal: 4,
+ },
+ versionText: {
+ fontSize: 12,
+ fontWeight: '500',
+ color: '#666',
+ },
+ versionCount: {
+ fontSize: 10,
+ fontWeight: '700',
+ color: '#fff',
+ backgroundColor: '#666',
+ width: 16,
+ height: 16,
+ borderRadius: 8,
+ textAlign: 'center',
+ lineHeight: 16,
+ marginLeft: 4,
+ },
+ actions: {
+ flexDirection: 'row',
+ },
+ actionButton: {
+ padding: 6,
+ marginLeft: 8,
+ },
+ contentContainer: {
+ padding: 16,
+ flex: 1,
+ paddingBottom: 60, // Extra padding für besseres Scrollen
+ },
+ content: {
+ fontSize: 15,
+ lineHeight: 22,
+ },
+ editor: {
+ flex: 1,
+ padding: 16,
+ fontSize: 15,
+ lineHeight: 24,
+ borderWidth: 1,
+ borderRadius: 8,
+ margin: 8,
+ textAlignVertical: 'top',
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
+ },
+ loadingText: {
+ marginTop: 10,
+ fontSize: 14,
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/components/DocumentVersions.tsx b/chat/apps/mobile/components/DocumentVersions.tsx
new file mode 100644
index 000000000..6fa4fa432
--- /dev/null
+++ b/chat/apps/mobile/components/DocumentVersions.tsx
@@ -0,0 +1,243 @@
+import React from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+ ScrollView,
+ Modal,
+ SafeAreaView,
+ Alert,
+} from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { Ionicons } from '@expo/vector-icons';
+import { Document } from '../services/document';
+
+interface DocumentVersionsProps {
+ isVisible: boolean;
+ documents: Document[];
+ onClose: () => void;
+ onSelectVersion: (document: Document) => void;
+ onDeleteVersion?: (document: Document) => void;
+}
+
+export default function DocumentVersions({
+ isVisible,
+ documents,
+ onClose,
+ onSelectVersion,
+ onDeleteVersion
+}: DocumentVersionsProps) {
+ const { colors } = useTheme();
+
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleString('de-DE', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ };
+
+ const renderVersionItem = (document: Document, isLatest: boolean) => {
+ // Löschen nur anzeigen, wenn es mehr als eine Version gibt und es nicht die neueste ist
+ // oder wenn es die einzige Version ist (nur zur Konsistenz)
+ const canDelete = documents.length > 1 || !isLatest;
+
+ return (
+
+ {
+ console.log('Version auswählen:', document.id);
+ onSelectVersion(document);
+ }}
+ >
+
+
+ v{document.version}
+
+ {isLatest && (
+
+ Aktuell
+
+ )}
+
+
+
+
+ {formatDate(document.created_at)}
+
+
+
+ {document.content.substring(0, 150)}
+ {document.content.length > 150 ? '...' : ''}
+
+
+
+ {/* Löschen-Button außerhalb der Touchable-Fläche für den Artikel */}
+ {canDelete && onDeleteVersion && (
+ {
+ console.log("Löschen-Button separat wurde gedrückt für:", document.id);
+
+ // Direkter Aufruf für Testzwecke
+ if (onDeleteVersion) {
+ console.log("Rufe onDeleteVersion direkt auf für Dokument ID:", document.id);
+ onDeleteVersion(document);
+
+ // Schließe das Modal nach einer kurzen Verzögerung
+ setTimeout(() => {
+ onClose();
+ }, 100);
+ } else {
+ console.error("onDeleteVersion ist nicht definiert!");
+ }
+ }}
+ >
+
+ Löschen
+
+ )}
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
+ Dokumentversionen
+
+
+
+ {documents.map((document, index) => renderVersionItem(document, index === 0))}
+
+ {documents.length === 0 && (
+
+
+
+ Keine Dokumentversionen verfügbar
+
+
+ )}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 16,
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(0,0,0,0.1)',
+ },
+ closeButton: {
+ padding: 8,
+ marginRight: 8,
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: '600',
+ },
+ versionsList: {
+ flex: 1,
+ },
+ versionItem: {
+ padding: 16,
+ borderBottomWidth: 1,
+ },
+ versionHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ versionBadge: {
+ backgroundColor: '#e0e0e0',
+ paddingHorizontal: 8,
+ paddingVertical: 3,
+ borderRadius: 12,
+ },
+ versionNumber: {
+ fontSize: 12,
+ fontWeight: '600',
+ color: '#333',
+ },
+ latestBadge: {
+ marginLeft: 8,
+ paddingHorizontal: 8,
+ paddingVertical: 3,
+ borderRadius: 12,
+ },
+ latestText: {
+ fontSize: 12,
+ fontWeight: '600',
+ color: 'white',
+ },
+ deleteSeparateButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginTop: 8,
+ marginHorizontal: 8,
+ paddingVertical: 8,
+ paddingHorizontal: 12,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: '#ff3b30',
+ },
+ deleteButtonText: {
+ color: 'red',
+ marginLeft: 6,
+ fontSize: 14,
+ fontWeight: '500',
+ },
+ versionDate: {
+ fontSize: 13,
+ marginBottom: 8,
+ },
+ versionPreview: {
+ fontSize: 14,
+ lineHeight: 20,
+ },
+ emptyState: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 40,
+ },
+ emptyText: {
+ marginTop: 16,
+ fontSize: 16,
+ textAlign: 'center',
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/components/EditScreenInfo.tsx b/chat/apps/mobile/components/EditScreenInfo.tsx
new file mode 100644
index 000000000..6fd8e54ed
--- /dev/null
+++ b/chat/apps/mobile/components/EditScreenInfo.tsx
@@ -0,0 +1,29 @@
+import { Text, View } from 'react-native';
+
+export const EditScreenInfo = ({ path }: { path: string }) => {
+ const title = 'Open up the code for this screen:';
+ const description =
+ 'Change any of the text, save the file, and your app will automatically update.';
+
+ return (
+
+
+ {title}
+
+ {path}
+
+ {description}
+
+
+ );
+};
+
+const styles = {
+ codeHighlightContainer: `rounded-md px-1`,
+ getStartedContainer: `items-center mx-12`,
+ getStartedText: `text-lg leading-6 text-center`,
+ helpContainer: `items-center mx-5 mt-4`,
+ helpLink: `py-4`,
+ helpLinkText: `text-center`,
+ homeScreenFilename: `my-2`,
+};
diff --git a/chat/apps/mobile/components/MessageInput.tsx b/chat/apps/mobile/components/MessageInput.tsx
new file mode 100644
index 000000000..23bdb6ba6
--- /dev/null
+++ b/chat/apps/mobile/components/MessageInput.tsx
@@ -0,0 +1,108 @@
+import React, { useState, forwardRef, useImperativeHandle, useRef } from 'react';
+import { View, TextInput, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { useTheme } from '@react-navigation/native';
+
+type MessageInputProps = {
+ onSend: (message: string) => void;
+ isLoading?: boolean;
+};
+
+// Öffentliche Methoden über Ref
+export interface MessageInputRef {
+ focus: () => void;
+}
+
+const MessageInput = forwardRef(
+ function MessageInput({ onSend, isLoading = false }, ref) {
+ const [message, setMessage] = useState('');
+ const { colors } = useTheme();
+ const inputRef = useRef(null);
+
+ // Stellt die focus-Methode über ref zur Verfügung
+ useImperativeHandle(ref, () => ({
+ focus: () => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }
+ }));
+
+ const handleSend = () => {
+ if (message.trim() && !isLoading) {
+ onSend(message.trim());
+ setMessage('');
+ }
+ };
+
+ // Tastatur-Event-Handler für Enter-Taste (besonders wichtig für Web)
+ const handleKeyPress = (e: any) => {
+ // Prüfen auf Enter ohne Shift für Submit
+ if (e.nativeEvent.key === 'Enter' && !e.nativeEvent.shiftKey) {
+ e.preventDefault(); // Verhindert Zeilenumbruch
+ handleSend();
+ }
+ };
+
+ return (
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ }
+);
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ borderTopWidth: StyleSheet.hairlineWidth,
+ borderTopColor: 'rgba(0,0,0,0.1)',
+ width: '100%',
+ maxWidth: 1200,
+ alignSelf: 'center',
+ },
+ input: {
+ flex: 1,
+ borderRadius: 20,
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ maxHeight: 120,
+ marginRight: 8,
+ },
+ sendButton: {
+ width: 44,
+ height: 44,
+ borderRadius: 22,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+});
+
+export default MessageInput;
\ No newline at end of file
diff --git a/chat/apps/mobile/components/MessageItem.tsx b/chat/apps/mobile/components/MessageItem.tsx
new file mode 100644
index 000000000..786ccbf63
--- /dev/null
+++ b/chat/apps/mobile/components/MessageItem.tsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import SkeletonLoader from './SkeletonLoader';
+import TypingIndicator from './TypingIndicator';
+
+type MessageProps = {
+ text: string;
+ sender: 'user' | 'ai';
+ timestamp: Date;
+ isLoading?: boolean;
+};
+
+export default function MessageItem({
+ text,
+ sender,
+ timestamp,
+ isLoading = false
+}: MessageProps) {
+ const { colors } = useTheme();
+
+ const isUser = sender === 'user';
+
+ return (
+
+ {isLoading && sender === 'ai' ? (
+ // Zeige Skeleton oder TypingIndicator wenn geladen wird
+ <>
+
+
+ >
+ ) : (
+ // Zeige die eigentliche Nachricht
+
+ {text}
+
+ )}
+
+
+ {timestamp.getHours().toString().padStart(2, '0')}:{timestamp.getMinutes().toString().padStart(2, '0')}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ padding: 12,
+ borderRadius: 16,
+ marginVertical: 4,
+ marginHorizontal: 12,
+ },
+ userContainer: {
+ maxWidth: '80%',
+ alignSelf: 'flex-start',
+ borderBottomLeftRadius: 4,
+ },
+ aiContainer: {
+ width: '95%',
+ alignSelf: 'flex-end',
+ borderBottomRightRadius: 4,
+ },
+ messageText: {
+ fontSize: 16,
+ lineHeight: 22,
+ },
+ timestamp: {
+ fontSize: 12,
+ marginTop: 4,
+ alignSelf: 'flex-end',
+ },
+ skeletonContainer: {
+ padding: 0,
+ margin: 0,
+ opacity: 0.8,
+ },
+ typingIndicator: {
+ marginLeft: -5,
+ marginTop: 5,
+ }
+});
diff --git a/chat/apps/mobile/components/MessageList.tsx b/chat/apps/mobile/components/MessageList.tsx
new file mode 100644
index 000000000..a0d5115de
--- /dev/null
+++ b/chat/apps/mobile/components/MessageList.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import { FlatList, StyleSheet, View } from 'react-native';
+import MessageItem from './MessageItem';
+
+type Message = {
+ id: string;
+ text: string;
+ sender: 'user' | 'ai';
+ timestamp: Date;
+ isLoading?: boolean;
+};
+
+type MessageListProps = {
+ messages: Message[];
+ isLoading?: boolean;
+};
+
+export default function MessageList({ messages, isLoading = false }: MessageListProps) {
+ const renderMessageItem = ({ item, index }: { item: Message, index: number }) => {
+ // Wenn die Nachricht die letzte ist und vom KI-Assistenten stammt,
+ // zeigen wir den Lade-Indikator an, wenn isLoading=true ist
+ const isLastMessage = index === messages.length - 1;
+ const isLastAIMessage = isLastMessage && item.sender === 'ai';
+ const shouldShowLoading = isLoading && isLastAIMessage;
+
+ return (
+
+ );
+ };
+
+ return (
+ item.id}
+ renderItem={renderMessageItem}
+ style={styles.container}
+ contentContainerStyle={styles.contentContainer}
+ inverted={false}
+ showsVerticalScrollIndicator={false}
+ ListFooterComponent={}
+ />
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ width: '100%',
+ maxWidth: 800,
+ alignSelf: 'center',
+ },
+ contentContainer: {
+ paddingVertical: 16,
+ paddingHorizontal: 16,
+ },
+ footer: {
+ height: 20,
+ },
+});
diff --git a/chat/apps/mobile/components/ModelCard.tsx b/chat/apps/mobile/components/ModelCard.tsx
new file mode 100644
index 000000000..4f543a952
--- /dev/null
+++ b/chat/apps/mobile/components/ModelCard.tsx
@@ -0,0 +1,122 @@
+import React from 'react';
+import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+import { Ionicons } from '@expo/vector-icons';
+import { Model } from '../types';
+
+type ModelCardProps = {
+ id: string;
+ name: string;
+ description: string;
+ deployment?: string;
+ isSelected?: boolean;
+ onSelect: (id: string) => void;
+ model?: Model; // Optionales komplettes Model-Objekt
+};
+
+export default function ModelCard({
+ id,
+ name,
+ description,
+ isSelected = false,
+ onSelect,
+ model
+}: ModelCardProps) {
+ const { colors } = useTheme();
+ const deployment = model?.parameters?.deployment;
+
+ return (
+ onSelect(id)}
+ >
+
+
+
+
+
+ {name}
+
+ {description}
+
+
+ {deployment && (
+
+ {deployment}
+
+ )}
+
+
+ {isSelected && (
+
+
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 16,
+ borderRadius: 12,
+ marginBottom: 12,
+ borderWidth: 2,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.05,
+ shadowRadius: 2,
+ elevation: 1,
+ },
+ iconContainer: {
+ width: 48,
+ height: 48,
+ borderRadius: 24,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: 'rgba(0,0,0,0.05)',
+ },
+ contentContainer: {
+ flex: 1,
+ marginLeft: 16,
+ },
+ name: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginBottom: 4,
+ },
+ description: {
+ fontSize: 14,
+ lineHeight: 20,
+ marginBottom: 4,
+ },
+ deployment: {
+ fontSize: 12,
+ fontWeight: '500',
+ },
+ checkmark: {
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+});
diff --git a/chat/apps/mobile/components/ModelDropdown.tsx b/chat/apps/mobile/components/ModelDropdown.tsx
new file mode 100644
index 000000000..6f39cc35a
--- /dev/null
+++ b/chat/apps/mobile/components/ModelDropdown.tsx
@@ -0,0 +1,161 @@
+import React, { useState, useEffect } from 'react';
+import { View, Text, TouchableOpacity, Modal, FlatList, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { useAppTheme } from '../theme/ThemeProvider';
+import { Model } from '../types';
+import { availableModels } from '../config/azure';
+import { getModels } from '../services/modelService';
+
+// Verwende Modelle aus der Konfiguration
+const FALLBACK_MODELS: Model[] = availableModels;
+
+type ModelDropdownProps = {
+ selectedModelId: string;
+ onSelectModel: (id: string) => void;
+};
+
+export default function ModelDropdown({ selectedModelId, onSelectModel }: ModelDropdownProps) {
+ const { isDarkMode } = useAppTheme();
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [models, setModels] = useState(FALLBACK_MODELS);
+ const [loading, setLoading] = useState(false);
+
+ // Lade die Modelle vom ModelService
+ useEffect(() => {
+ const fetchModels = async () => {
+ try {
+ setLoading(true);
+ const modelsList = await getModels();
+ setModels(modelsList);
+ } catch (err) {
+ console.error('Fehler beim Laden der Modelle:', err);
+ setModels(FALLBACK_MODELS);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchModels();
+ }, []);
+
+ const selectedModel = models.find(model => model.id === selectedModelId) || models[0];
+
+ return (
+
+ setIsModalVisible(true)}
+ className={`flex-row items-center rounded-lg px-2 py-1 ${isDarkMode ? 'bg-[#2C2C2E]' : 'bg-gray-100'}`}
+ >
+
+ {selectedModel.name}
+
+
+
+
+ setIsModalVisible(false)}
+ >
+ setIsModalVisible(false)}
+ >
+
+
+ Modell auswählen
+
+
+ {loading ? (
+
+
+ Modelle werden geladen...
+
+
+ ) : (
+ item.id}
+ renderItem={({ item }) => (
+ {
+ onSelectModel(item.id);
+ setIsModalVisible(false);
+ }}
+ >
+
+
+
+
+
+
+ {item.name}
+
+
+ {item.description}
+
+ {item.parameters?.deployment && (
+
+ {item.parameters.deployment}
+
+ )}
+
+
+ {item.id === selectedModelId && (
+
+
+
+ )}
+
+ )}
+ />
+ )}
+
+ setIsModalVisible(false)}
+ >
+ Schließen
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ modalOverlay: {
+ flex: 1,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ justifyContent: 'center',
+ },
+ modalContent: {
+ maxHeight: '80%',
+ },
+});
diff --git a/chat/apps/mobile/components/NewChatButton.tsx b/chat/apps/mobile/components/NewChatButton.tsx
new file mode 100644
index 000000000..2428b4cee
--- /dev/null
+++ b/chat/apps/mobile/components/NewChatButton.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { TouchableOpacity, Text, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { useTheme } from '@react-navigation/native';
+
+type NewChatButtonProps = {
+ onPress: () => void;
+};
+
+export default function NewChatButton({ onPress }: NewChatButtonProps) {
+ const { colors } = useTheme();
+
+ return (
+
+
+ Neuer Chat
+
+ );
+}
+
+const styles = StyleSheet.create({
+ button: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 14,
+ paddingHorizontal: 24,
+ borderRadius: 30,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ },
+ icon: {
+ marginRight: 8,
+ },
+ text: {
+ color: '#fff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+});
diff --git a/chat/apps/mobile/components/SkeletonLoader.tsx b/chat/apps/mobile/components/SkeletonLoader.tsx
new file mode 100644
index 000000000..c44de0ec7
--- /dev/null
+++ b/chat/apps/mobile/components/SkeletonLoader.tsx
@@ -0,0 +1,81 @@
+import React, { useEffect, useState } from 'react';
+import { View, Animated, Easing, StyleSheet } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+
+type SkeletonLoaderProps = {
+ lines?: number;
+ animated?: boolean;
+ style?: any;
+};
+
+export default function SkeletonLoader({
+ lines = 3,
+ animated = true,
+ style
+}: SkeletonLoaderProps) {
+ const { colors } = useTheme();
+ const [fadeAnim] = useState(new Animated.Value(0.3));
+
+ useEffect(() => {
+ if (animated) {
+ Animated.loop(
+ Animated.sequence([
+ Animated.timing(fadeAnim, {
+ toValue: 0.8,
+ duration: 800,
+ easing: Easing.inOut(Easing.ease),
+ useNativeDriver: true,
+ }),
+ Animated.timing(fadeAnim, {
+ toValue: 0.3,
+ duration: 800,
+ easing: Easing.inOut(Easing.ease),
+ useNativeDriver: true,
+ }),
+ ])
+ ).start();
+ }
+ }, [fadeAnim, animated]);
+
+ // Erstelle verschiedene Längen für die Zeilen
+ const getRandomWidth = (index: number) => {
+ // Erste und letzte Zeile sind kürzer
+ if (index === 0) return { width: '70%' };
+ if (index === lines - 1) return { width: '40%' };
+
+ // Zufällige Breite für die Zeilen dazwischen
+ const widths = ['85%', '90%', '75%', '95%'];
+ return { width: widths[index % widths.length] };
+ };
+
+ return (
+
+ {Array.from({ length: lines }).map((_, index) => (
+
+ ))}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ padding: 16,
+ maxWidth: '80%',
+ alignSelf: 'flex-start',
+ },
+ line: {
+ height: 15,
+ borderRadius: 4,
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/components/TemplateCard.tsx b/chat/apps/mobile/components/TemplateCard.tsx
new file mode 100644
index 000000000..e9908349b
--- /dev/null
+++ b/chat/apps/mobile/components/TemplateCard.tsx
@@ -0,0 +1,180 @@
+import React from 'react';
+import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { useTheme } from '@react-navigation/native';
+import { useAppTheme } from '../theme/ThemeProvider';
+
+// Typ für die Template-Props
+interface TemplateCardProps {
+ id: string;
+ name: string;
+ description?: string | null;
+ systemPrompt: string;
+ color?: string;
+ isDefault?: boolean;
+ onPress: (id: string) => void;
+ onEdit?: (id: string) => void;
+ onDelete?: (id: string) => void;
+ onSetDefault?: (id: string) => void;
+}
+
+export default function TemplateCard({
+ id,
+ name,
+ description,
+ systemPrompt,
+ color = '#0A84FF',
+ isDefault = false,
+ onPress,
+ onEdit,
+ onDelete,
+ onSetDefault
+}: TemplateCardProps) {
+ const { colors } = useTheme();
+ const { isDarkMode } = useAppTheme();
+
+ const backgroundColor = isDarkMode ? '#2C2C2E' : '#FFFFFF';
+ const textColor = isDarkMode ? '#FFFFFF' : '#000000';
+ const secondaryTextColor = isDarkMode ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)';
+
+ // Kürze den System-Prompt für die Anzeige
+ const truncatedPrompt = systemPrompt.length > 80
+ ? systemPrompt.substring(0, 80) + '...'
+ : systemPrompt;
+
+ return (
+ onPress(id)}
+ >
+ {/* Farbiger Indikator am linken Rand */}
+
+
+
+
+ {name}
+
+ {isDefault && (
+
+ Standard
+
+ )}
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+ {truncatedPrompt}
+
+
+
+ {/* Aktionen */}
+
+ {onSetDefault && !isDefault && (
+ onSetDefault(id)}
+ >
+
+
+ )}
+
+ {onEdit && (
+ onEdit(id)}
+ >
+
+
+ )}
+
+ {onDelete && (
+ onDelete(id)}
+ >
+
+
+ )}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ borderRadius: 12,
+ marginBottom: 12,
+ overflow: 'hidden',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ },
+ defaultContainer: {
+ borderWidth: 1,
+ borderColor: '#0A84FF',
+ },
+ colorIndicator: {
+ width: 8,
+ alignSelf: 'stretch',
+ },
+ content: {
+ flex: 1,
+ padding: 16,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginBottom: 8,
+ },
+ name: {
+ fontSize: 16,
+ fontWeight: '600',
+ flex: 1,
+ },
+ defaultBadge: {
+ backgroundColor: '#0A84FF',
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ borderRadius: 4,
+ marginLeft: 8,
+ },
+ defaultText: {
+ color: 'white',
+ fontSize: 10,
+ fontWeight: '600',
+ },
+ description: {
+ fontSize: 14,
+ marginBottom: 8,
+ },
+ prompt: {
+ fontSize: 12,
+ fontStyle: 'italic',
+ },
+ actions: {
+ padding: 8,
+ justifyContent: 'center',
+ },
+ actionButton: {
+ padding: 8,
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/components/TemplateForm.tsx b/chat/apps/mobile/components/TemplateForm.tsx
new file mode 100644
index 000000000..f76a4935c
--- /dev/null
+++ b/chat/apps/mobile/components/TemplateForm.tsx
@@ -0,0 +1,417 @@
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ StyleSheet,
+ ScrollView,
+ KeyboardAvoidingView,
+ Platform,
+ Alert
+} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { useTheme } from '@react-navigation/native';
+import { useAppTheme } from '../theme/ThemeProvider';
+import ModelDropdown from './ModelDropdown';
+import { Template } from '../services/template';
+
+// Verfügbare Farben für Vorlagen
+const TEMPLATE_COLORS = [
+ '#0A84FF', // Blau
+ '#32D74B', // Grün
+ '#FF375F', // Rot
+ '#FF9F0A', // Orange
+ '#5E5CE6', // Lila
+ '#BF5AF2', // Pink
+ '#64D2FF', // Hellblau
+ '#30D158', // Grün
+ '#FF453A', // Rot
+];
+
+interface TemplateFormProps {
+ initialData?: Partial;
+ onSubmit: (data: Partial) => void;
+ onCancel: () => void;
+}
+
+export default function TemplateForm({
+ initialData,
+ onSubmit,
+ onCancel
+}: TemplateFormProps) {
+ const { colors } = useTheme();
+ const { isDarkMode } = useAppTheme();
+
+ // Form state
+ const [name, setName] = useState(initialData?.name || '');
+ const [description, setDescription] = useState(initialData?.description || '');
+ const [systemPrompt, setSystemPrompt] = useState(initialData?.system_prompt || '');
+ const [initialQuestion, setInitialQuestion] = useState(initialData?.initial_question || '');
+ const [selectedColor, setSelectedColor] = useState(initialData?.color || TEMPLATE_COLORS[0]);
+ const [selectedModelId, setSelectedModelId] = useState(initialData?.model_id || '');
+ const [documentMode, setDocumentMode] = useState(initialData?.document_mode || false);
+
+ // Validierung
+ const [errors, setErrors] = useState<{
+ name?: string;
+ systemPrompt?: string;
+ }>({});
+
+ // Helpers
+ const isEditMode = !!initialData?.id;
+ const bgColor = isDarkMode ? '#1C1C1E' : '#FFFFFF';
+ const textColor = isDarkMode ? '#FFFFFF' : '#000000';
+ const placeholderColor = isDarkMode ? '#8E8E93' : '#C7C7CC';
+ const borderColor = isDarkMode ? '#38383A' : '#E5E5EA';
+
+ // Validiere das Formular vor dem Absenden
+ const validateForm = (): boolean => {
+ const newErrors: {
+ name?: string;
+ systemPrompt?: string;
+ } = {};
+
+ if (!name.trim()) {
+ newErrors.name = 'Bitte gib einen Namen ein.';
+ }
+
+ if (!systemPrompt.trim()) {
+ newErrors.systemPrompt = 'Der System-Prompt darf nicht leer sein.';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ // Handle submit
+ const handleSubmit = () => {
+ if (!validateForm()) return;
+
+ onSubmit({
+ id: initialData?.id,
+ name,
+ description: description.trim() || null,
+ system_prompt: systemPrompt,
+ initial_question: initialQuestion.trim() || null,
+ color: selectedColor,
+ model_id: selectedModelId || null,
+ document_mode: documentMode
+ });
+ };
+
+ return (
+
+
+
+ {/* Titel */}
+
+ {isEditMode ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen'}
+
+
+ {/* Name */}
+
+ Name *
+
+ {errors.name && (
+ {errors.name}
+ )}
+
+
+ {/* Beschreibung */}
+
+ Beschreibung (optional)
+
+
+
+ {/* System-Prompt */}
+
+ System-Prompt *
+
+ {errors.systemPrompt && (
+ {errors.systemPrompt}
+ )}
+
+ Der System-Prompt definiert die Rolle und das Verhalten der KI.
+
+
+
+ {/* Initiale Frage */}
+
+ Beispielfrage (optional)
+
+
+ Diese Frage wird als Vorschlag angezeigt, wenn die Vorlage ausgewählt wird.
+
+
+
+ {/* Farbe auswählen */}
+
+ Farbe
+
+ {TEMPLATE_COLORS.map((color) => (
+ setSelectedColor(color)}
+ >
+ {selectedColor === color && (
+
+ )}
+
+ ))}
+
+
+
+ {/* Modell auswählen */}
+
+ Bevorzugtes Modell (optional)
+
+
+ Falls ausgewählt, wird dieses Modell automatisch mit der Vorlage verwendet.
+
+
+
+ {/* Dokumentmodus */}
+
+ Dokumentmodus
+ setDocumentMode(!documentMode)}
+ >
+
+
+ Dokumentmodus aktivieren
+
+
+ Ermöglicht die Bearbeitung eines Dokuments während der Konversation
+
+
+
+ {documentMode ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Buttons */}
+
+
+ Abbrechen
+
+
+
+
+ {isEditMode ? 'Speichern' : 'Erstellen'}
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ scrollView: {
+ flex: 1,
+ },
+ form: {
+ padding: 20,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ marginBottom: 20,
+ },
+ formGroup: {
+ marginBottom: 20,
+ },
+ label: {
+ fontSize: 16,
+ fontWeight: '500',
+ marginBottom: 8,
+ },
+ input: {
+ borderWidth: 1,
+ borderRadius: 8,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ fontSize: 16,
+ },
+ textArea: {
+ height: 80,
+ textAlignVertical: 'top',
+ },
+ helperText: {
+ fontSize: 12,
+ marginTop: 6,
+ },
+ errorText: {
+ fontSize: 12,
+ color: '#FF3B30',
+ marginTop: 6,
+ },
+ colorPicker: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ marginTop: 10,
+ },
+ colorOption: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ margin: 5,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ selectedColorOption: {
+ borderWidth: 2,
+ borderColor: 'white',
+ },
+ switchContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ borderWidth: 1,
+ borderRadius: 8,
+ padding: 12,
+ },
+ switchText: {
+ flex: 1,
+ marginRight: 12,
+ },
+ switchLabel: {
+ fontSize: 16,
+ fontWeight: '500',
+ marginBottom: 4,
+ },
+ switchDescription: {
+ fontSize: 12,
+ },
+ switchButton: {
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ buttonContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginTop: 30,
+ },
+ button: {
+ paddingVertical: 12,
+ paddingHorizontal: 20,
+ borderRadius: 8,
+ minWidth: 120,
+ alignItems: 'center',
+ },
+ cancelButton: {
+ borderWidth: 1,
+ },
+ submitButton: {
+ backgroundColor: '#0A84FF',
+ },
+ buttonText: {
+ fontSize: 16,
+ fontWeight: '500',
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/components/TypingIndicator.tsx b/chat/apps/mobile/components/TypingIndicator.tsx
new file mode 100644
index 000000000..4f3f42710
--- /dev/null
+++ b/chat/apps/mobile/components/TypingIndicator.tsx
@@ -0,0 +1,103 @@
+import React, { useEffect, useState } from 'react';
+import { View, Text, StyleSheet, Animated, Easing } from 'react-native';
+import { useTheme } from '@react-navigation/native';
+
+type TypingIndicatorProps = {
+ dotCount?: number;
+ dotSize?: number;
+ dotColor?: string;
+ style?: any;
+};
+
+export default function TypingIndicator({
+ dotCount = 3,
+ dotSize = 8,
+ dotColor,
+ style,
+}: TypingIndicatorProps) {
+ const { colors } = useTheme();
+ const [animations] = useState(() =>
+ Array.from({ length: dotCount }).map(() => new Animated.Value(0))
+ );
+
+ // Dotfarbe wird entweder von Prop oder vom Theme übernommen
+ const actualDotColor = dotColor || colors.text;
+
+ useEffect(() => {
+ // Animiere jeden Punkt mit einer Verzögerung
+ const animateDots = () => {
+ const animationSequence = animations.map((anim, i) =>
+ Animated.sequence([
+ // Verzögerung für jeden Punkt
+ Animated.delay(i * 150),
+ // Animation nach oben
+ Animated.timing(anim, {
+ toValue: 1,
+ duration: 400,
+ easing: Easing.inOut(Easing.ease),
+ useNativeDriver: true,
+ }),
+ // Animation zurück nach unten
+ Animated.timing(anim, {
+ toValue: 0,
+ duration: 400,
+ easing: Easing.inOut(Easing.ease),
+ useNativeDriver: true,
+ }),
+ // Verzögerung am Ende
+ Animated.delay((dotCount - i - 1) * 150),
+ ])
+ );
+
+ // Starte alle Animationen parallel und in einer Schleife
+ Animated.loop(Animated.parallel(animationSequence)).start();
+ };
+
+ animateDots();
+
+ // Cleanup beim Unmount
+ return () => {
+ animations.forEach(anim => anim.stopAnimation());
+ };
+ }, [animations, dotCount]);
+
+ return (
+
+ {animations.map((anim, index) => (
+
+ ))}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 10,
+ },
+ dot: {
+ opacity: 0.6,
+ },
+});
\ No newline at end of file
diff --git a/chat/apps/mobile/config/azure.ts b/chat/apps/mobile/config/azure.ts
new file mode 100644
index 000000000..b6bbf5d0f
--- /dev/null
+++ b/chat/apps/mobile/config/azure.ts
@@ -0,0 +1,56 @@
+/**
+ * Model Configuration
+ * Note: API keys are now stored securely in the backend
+ * This file only contains model definitions for the mobile app UI
+ */
+
+// Available models for the chat application
+// These match the models configured in the backend
+export const availableModels = [
+ {
+ id: '550e8400-e29b-41d4-a716-446655440000',
+ name: 'GPT-O3-Mini',
+ description: 'Azure OpenAI O3-Mini: Effizientes Modell für schnelle Antworten.',
+ parameters: {
+ temperature: 0.7,
+ max_tokens: 800,
+ provider: 'azure',
+ deployment: 'gpt-o3-mini-se',
+ }
+ },
+ {
+ id: '550e8400-e29b-41d4-a716-446655440004',
+ name: 'GPT-4o-Mini',
+ description: 'Azure OpenAI GPT-4o-Mini: Kompaktes, leistungsstarkes KI-Modell.',
+ parameters: {
+ temperature: 0.7,
+ max_tokens: 1000,
+ provider: 'azure',
+ deployment: 'gpt-4o-mini-se',
+ }
+ },
+ {
+ id: '550e8400-e29b-41d4-a716-446655440005',
+ name: 'GPT-4o',
+ description: 'Azure OpenAI GPT-4o: Das fortschrittlichste multimodale KI-Modell.',
+ parameters: {
+ temperature: 0.7,
+ max_tokens: 1200,
+ provider: 'azure',
+ deployment: 'gpt-4o-se',
+ }
+ }
+];
+
+// Helper function to get model by ID
+export function getModelById(modelId: string) {
+ return availableModels.find(m => m.id === modelId);
+}
+
+// Helper function to get model by deployment name
+export function getModelByDeployment(deployment: string) {
+ return availableModels.find(m => m.parameters.deployment === deployment);
+}
+
+// Default model
+export const defaultModel = availableModels[0];
diff --git a/chat/apps/mobile/context/AuthProvider.tsx b/chat/apps/mobile/context/AuthProvider.tsx
new file mode 100644
index 000000000..fd460f510
--- /dev/null
+++ b/chat/apps/mobile/context/AuthProvider.tsx
@@ -0,0 +1,161 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { supabase } from '../utils/supabase';
+import { Session, User } from '@supabase/supabase-js';
+import { ActivityIndicator, View, Text } from 'react-native';
+
+// Definiere den Typ für den Auth-Kontext
+type AuthContextType = {
+ session: Session | null;
+ user: User | null;
+ loading: boolean;
+ signIn: (email: string, password: string) => Promise<{ error: any | null }>;
+ signUp: (email: string, password: string) => Promise<{ error: any | null, data: any | null }>;
+ signOut: () => Promise;
+ resetPassword: (email: string) => Promise<{ error: any | null }>;
+};
+
+// Erstelle den Auth-Kontext
+const AuthContext = createContext(undefined);
+
+// Hook für den Zugriff auf den Auth-Kontext
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (context === undefined) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+};
+
+// AuthProvider-Komponente
+export function AuthProvider({ children }: { children: React.ReactNode }) {
+ const [session, setSession] = useState(null);
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ // Initialisiere den Auth-Status
+ useEffect(() => {
+ // Hole die aktuelle Session
+ const getInitialSession = async () => {
+ try {
+ setLoading(true);
+
+ // Prüfe, ob bereits eine Session existiert
+ const { data: { session } } = await supabase.auth.getSession();
+ setSession(session);
+ setUser(session?.user ?? null);
+
+ // Abonniere Änderungen am Auth-Status
+ const { data: { subscription } } = await supabase.auth.onAuthStateChange(
+ (_event, session) => {
+ setSession(session);
+ setUser(session?.user ?? null);
+ }
+ );
+
+ return () => {
+ subscription.unsubscribe();
+ };
+ } catch (error) {
+ console.error('Fehler beim Initialisieren der Auth-Session:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ getInitialSession();
+ }, []);
+
+ // Anmelden mit E-Mail und Passwort
+ const signIn = async (email: string, password: string) => {
+ try {
+ console.log('Versuche Anmeldung mit:', email);
+ const { data, error } = await supabase.auth.signInWithPassword({ email, password });
+
+ if (error) {
+ console.error('Supabase Auth Fehler:', error.message, error.status);
+ return { error };
+ }
+
+ console.log('Anmeldung erfolgreich:', data.user?.id);
+ return { error: null };
+ } catch (error: any) {
+ console.error('Unerwarteter Fehler beim Anmelden:', error.message || error);
+ return { error };
+ }
+ };
+
+ // Registrieren mit E-Mail und Passwort
+ const signUp = async (email: string, password: string) => {
+ try {
+ // Registriere den Benutzer mit autoConfirm=true, um die E-Mail-Bestätigung zu umgehen
+ const { data, error } = await supabase.auth.signUp({
+ email,
+ password,
+ options: {
+ data: {
+ email_confirmed: true
+ }
+ }
+ });
+
+ if (!error && data?.user) {
+ // Wenn die Registrierung erfolgreich war, melde den Benutzer direkt an
+ await signIn(email, password);
+ }
+
+ return { data, error };
+ } catch (error) {
+ console.error('Fehler beim Registrieren:', error);
+ return { data: null, error };
+ }
+ };
+
+ // Abmelden
+ const signOut = async () => {
+ try {
+ await supabase.auth.signOut();
+ } catch (error) {
+ console.error('Fehler beim Abmelden:', error);
+ }
+ };
+
+ // Passwort zurücksetzen
+ const resetPassword = async (email: string) => {
+ try {
+ const { error } = await supabase.auth.resetPasswordForEmail(email, {
+ redirectTo: 'exp://localhost:8081/reset-password',
+ });
+ return { error };
+ } catch (error) {
+ console.error('Fehler beim Zurücksetzen des Passworts:', error);
+ return { error };
+ }
+ };
+
+ // Zeige Ladeindikator während der Initialisierung
+ if (loading) {
+ return (
+
+
+ Authentifizierung wird initialisiert...
+
+ );
+ }
+
+ // Stelle den Auth-Kontext bereit
+ return (
+
+ {children}
+
+ );
+}
diff --git a/chat/apps/mobile/eas.json b/chat/apps/mobile/eas.json
new file mode 100644
index 000000000..3ac72898e
--- /dev/null
+++ b/chat/apps/mobile/eas.json
@@ -0,0 +1,21 @@
+{
+ "cli": {
+ "version": ">= 15.0.15",
+ "appVersionSource": "remote"
+ },
+ "build": {
+ "development": {
+ "developmentClient": true,
+ "distribution": "internal"
+ },
+ "preview": {
+ "distribution": "internal"
+ },
+ "production": {
+ "autoIncrement": true
+ }
+ },
+ "submit": {
+ "production": {}
+ }
+}
diff --git a/chat/apps/mobile/global.css b/chat/apps/mobile/global.css
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/chat/apps/mobile/global.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/chat/apps/mobile/hooks/useChatInput.ts b/chat/apps/mobile/hooks/useChatInput.ts
new file mode 100644
index 000000000..53c8bbc1c
--- /dev/null
+++ b/chat/apps/mobile/hooks/useChatInput.ts
@@ -0,0 +1,50 @@
+import { useState } from 'react';
+import { useAppTheme } from '../theme/ThemeProvider';
+
+interface UseChatInputProps {
+ onSend: (message: string) => void;
+ isLoading?: boolean;
+ initialText?: string;
+ placeholder?: string;
+ maxLength?: number;
+}
+
+interface UseChatInputReturn {
+ text: string;
+ setText: (text: string) => void;
+ handleSend: () => void;
+ canSend: boolean;
+ isLoading: boolean;
+ isDarkMode: boolean;
+ placeholder: string;
+}
+
+export default function useChatInput({
+ onSend,
+ isLoading = false,
+ initialText = '',
+ placeholder = 'Nachricht eingeben...',
+ maxLength = 1000,
+}: UseChatInputProps): UseChatInputReturn {
+ const [text, setText] = useState(initialText);
+ const { isDarkMode } = useAppTheme();
+
+ const canSend = text.trim().length > 0 && !isLoading;
+
+ const handleSend = () => {
+ if (canSend) {
+ onSend(text.trim());
+ setText('');
+ }
+ };
+
+ return {
+ text,
+ setText,
+ handleSend,
+ canSend,
+ isLoading,
+ isDarkMode,
+ placeholder,
+ };
+}
\ No newline at end of file
diff --git a/chat/apps/mobile/metro.config.js b/chat/apps/mobile/metro.config.js
new file mode 100644
index 000000000..1a40036ee
--- /dev/null
+++ b/chat/apps/mobile/metro.config.js
@@ -0,0 +1,9 @@
+// Learn more https://docs.expo.io/guides/customizing-metro
+const { getDefaultConfig } = require('expo/metro-config');
+const { withNativeWind } = require('nativewind/metro');
+
+/** @type {import('expo/metro-config').MetroConfig} */
+// eslint-disable-next-line no-undef
+const config = getDefaultConfig(__dirname);
+
+module.exports = withNativeWind(config, { input: './global.css' });
diff --git a/chat/apps/mobile/nativewind-env.d.ts b/chat/apps/mobile/nativewind-env.d.ts
new file mode 100644
index 000000000..c0d838073
--- /dev/null
+++ b/chat/apps/mobile/nativewind-env.d.ts
@@ -0,0 +1,3 @@
+///
+
+// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.
\ No newline at end of file
diff --git a/chat/apps/mobile/package.json b/chat/apps/mobile/package.json
new file mode 100644
index 000000000..157e28317
--- /dev/null
+++ b/chat/apps/mobile/package.json
@@ -0,0 +1,69 @@
+{
+ "name": "@chat/mobile",
+ "version": "1.0.0",
+ "main": "expo-router/entry",
+ "type": "commonjs",
+ "scripts": {
+ "dev": "expo start --dev-client",
+ "start": "expo start --dev-client",
+ "ios": "expo run:ios",
+ "android": "expo run:android",
+ "build:dev": "eas build --profile development",
+ "build:preview": "eas build --profile preview",
+ "build:prod": "eas build --profile production",
+ "prebuild": "expo prebuild",
+ "lint": "eslint \"**/*.{js,jsx,ts,tsx}\" && prettier -c \"**/*.{js,jsx,ts,tsx,json}\"",
+ "format": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix && prettier \"**/*.{js,jsx,ts,tsx,json}\" --write",
+ "web": "expo start --web",
+ "supabase:cli": "node --experimental-json-modules scripts/supabase-cli.js",
+ "supabase:update-models": "node --experimental-json-modules scripts/update_models.js",
+ "supabase:setup": "node --experimental-json-modules scripts/setup_supabase.js",
+ "supabase:setup-spaces": "node --experimental-json-modules scripts/spaces/setup_spaces.js"
+ },
+ "dependencies": {
+ "@expo/vector-icons": "^14.0.0",
+ "@react-native-async-storage/async-storage": "1.23.1",
+ "@react-navigation/bottom-tabs": "^7.0.5",
+ "@react-navigation/drawer": "^7.0.0",
+ "@react-navigation/native": "^7.0.3",
+ "@supabase/supabase-js": "^2.38.4",
+ "expo": "^52.0.39",
+ "expo-constants": "~17.0.8",
+ "expo-dev-client": "~5.0.4",
+ "expo-dev-launcher": "^5.0.17",
+ "expo-linking": "~7.0.5",
+ "expo-router": "~4.0.6",
+ "expo-status-bar": "~2.0.1",
+ "expo-system-ui": "~4.0.8",
+ "expo-web-browser": "~14.0.2",
+ "nativewind": "latest",
+ "node-fetch": "^2.7.0",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "react-native": "0.76.7",
+ "react-native-gesture-handler": "~2.20.2",
+ "react-native-markdown-display": "^7.0.2",
+ "react-native-reanimated": "3.16.2",
+ "react-native-safe-area-context": "4.12.0",
+ "react-native-screens": "~4.4.0",
+ "react-native-web": "~0.19.10"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.20.0",
+ "@types/react": "~18.3.12",
+ "@typescript-eslint/eslint-plugin": "^7.7.0",
+ "@typescript-eslint/parser": "^7.7.0",
+ "dotenv": "^16.4.7",
+ "eslint": "^8.57.0",
+ "eslint-config-universe": "^12.0.1",
+ "prettier": "^3.2.5",
+ "prettier-plugin-tailwindcss": "^0.5.11",
+ "tailwindcss": "^3.4.0",
+ "typescript": "~5.3.3"
+ },
+ "eslintConfig": {
+ "extends": "universe/native",
+ "root": true
+ },
+ "private": true
+}
diff --git a/chat/apps/mobile/prettier.config.js b/chat/apps/mobile/prettier.config.js
new file mode 100644
index 000000000..17c1b8cc1
--- /dev/null
+++ b/chat/apps/mobile/prettier.config.js
@@ -0,0 +1,10 @@
+module.exports = {
+ printWidth: 100,
+ tabWidth: 2,
+ singleQuote: true,
+ bracketSameLine: true,
+ trailingComma: 'es5',
+
+ plugins: [require.resolve('prettier-plugin-tailwindcss')],
+ tailwindAttributes: ['className'],
+};
diff --git a/chat/apps/mobile/readme/ExpoApiRoutes.md b/chat/apps/mobile/readme/ExpoApiRoutes.md
new file mode 100644
index 000000000..638194508
--- /dev/null
+++ b/chat/apps/mobile/readme/ExpoApiRoutes.md
@@ -0,0 +1,561 @@
+API Routes
+Learn how to create server endpoints with Expo Router.
+
+
+Expo Router enables you to write secure server code for all platforms, right in your app directory.
+
+app/hello+api.ts
+
+Copy
+
+
+export function GET(request: Request) {
+ return Response.json({ hello: 'world' });
+}
+Server features require a custom server, which can be deployed to EAS or most other hosting providers.
+
+Watch: Expo Router API Routes Handle Requests & Stream Data
+Watch: Expo Router API Routes Handle Requests & Stream Data
+What are API Routes
+API Routes are functions that are executed on a server when a route is matched. They can be used to handle sensitive data, such as API keys securely, or implement custom server logic, such as exchanging auth codes for access tokens. API Routes should be executed in a WinterCG-compliant environment.
+
+In Expo, API Routes are defined by creating files in the app directory with the +api.ts extension. For example, the following API route is executed when the route /hello is matched.
+
+app
+
+index.tsx
+
+hello+api.ts
+API Route
+Create an API route
+1
+
+Ensure your project is using server output, this will configure the export and production builds to generate a server bundle as well as the client bundle.
+
+app.json
+
+Copy
+
+
+{
+ "web": {
+ "output": "server"
+ }
+}
+2
+
+An API route is created in the app directory. For example, add the following route handler. It is executed when the route /hello is matched.
+
+app/hello+api.ts
+
+Copy
+
+
+export function GET(request: Request) {
+ return Response.json({ hello: 'world' });
+}
+You can export any of the following functions GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS from a server route. The function executes when the corresponding HTTP method is matched. Unsupported methods will automatically return 405: Method not allowed.
+
+3
+
+Start the development server with Expo CLI:
+
+Terminal
+
+Copy
+
+npx expo
+4
+
+You can make a network request to the route to access the data. Run the following command to test the route:
+
+Terminal
+
+Copy
+
+curl http://localhost:8081/hello
+You can also make a request from the client code:
+
+app/index.tsx
+
+Copy
+
+
+import { Button } from 'react-native';
+
+async function fetchHello() {
+ const response = await fetch('/hello');
+ const data = await response.json();
+ alert('Hello ' + data.hello);
+}
+
+export default function App() {
+ return