mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 14:26:42 +02:00
style: auto-format codebase with Prettier
Applied formatting to 1487+ files using pnpm format:write - TypeScript/JavaScript files - Svelte components - Astro pages - JSON configs - Markdown docs 13 files still need manual review (Astro JSX comments)
This commit is contained in:
parent
0241f5554c
commit
d36b321d9d
3952 changed files with 661498 additions and 739751 deletions
|
|
@ -1,6 +1,7 @@
|
|||
# Claude's Guide to Chat Mobile App
|
||||
|
||||
## Commands
|
||||
|
||||
- Start app: `pnpm dev` or `pnpm start`
|
||||
- iOS: `pnpm ios`
|
||||
- Android: `pnpm android`
|
||||
|
|
@ -12,6 +13,7 @@
|
|||
## 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
|
||||
|
|
@ -19,12 +21,14 @@
|
|||
- `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
|
||||
|
|
|
|||
|
|
@ -21,20 +21,24 @@ Eine moderne mobile Chat-Anwendung zur Interaktion mit verschiedenen KI-Sprachmo
|
|||
## Einrichtung
|
||||
|
||||
1. Repository klonen
|
||||
|
||||
```
|
||||
git clone <repository-url>
|
||||
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
|
||||
|
|
@ -53,6 +57,7 @@ Eine moderne mobile Chat-Anwendung zur Interaktion mit verschiedenen KI-Sprachmo
|
|||
## Nutzung
|
||||
|
||||
Nach dem Start kannst du:
|
||||
|
||||
- Dich registrieren oder anmelden
|
||||
- Ein KI-Modell auswählen
|
||||
- Eine neue Konversation starten
|
||||
|
|
@ -60,4 +65,4 @@ Nach dem Start kannst du:
|
|||
|
||||
## Lizenz
|
||||
|
||||
MIT
|
||||
MIT
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ Basierend auf der Codeanalyse schlage ich folgende Maßnahmen zur Vereinfachung
|
|||
|
||||
## 3. Architektur-Optimierung
|
||||
|
||||
- **State Management**:
|
||||
- **State Management**:
|
||||
- Auth-Zustand über einen zentralen Store verwalten
|
||||
- Modell- und Konversationszustand aus UI-Komponenten in Services verlagern
|
||||
|
||||
|
|
@ -52,4 +52,4 @@ Basierend auf der Codeanalyse schlage ich folgende Maßnahmen zur Vereinfachung
|
|||
2. API-Wrapper erstellen
|
||||
3. State Management umstellen
|
||||
4. UI-Komponenten vereinheitlichen
|
||||
5. Styling standardisieren
|
||||
5. Styling standardisieren
|
||||
|
|
|
|||
|
|
@ -3,36 +3,43 @@
|
|||
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
|
||||
4. **Bessere Wartbarkeit**: klare Trennung zwischen Datenzugriff und UI
|
||||
|
|
|
|||
|
|
@ -1,56 +1,54 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,75 +5,75 @@ 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',
|
||||
},
|
||||
};
|
||||
const { isDarkMode } = useAppTheme();
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
drawerStyle: {
|
||||
backgroundColor: drawerStyles.backgroundColor,
|
||||
},
|
||||
drawerActiveTintColor: drawerStyles.contentOptions.activeTintColor,
|
||||
drawerInactiveTintColor: drawerStyles.contentOptions.inactiveTintColor,
|
||||
drawerActiveBackgroundColor: drawerStyles.contentOptions.activeBackgroundColor,
|
||||
}}
|
||||
>
|
||||
<Drawer.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Chat',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Ionicons name="chatbubbles-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="documents"
|
||||
options={{
|
||||
title: 'Dokumente',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Ionicons name="document-text-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="archive"
|
||||
options={{
|
||||
title: 'Archiv',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Ionicons name="archive-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="templates"
|
||||
options={{
|
||||
title: 'Vorlagen',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Ionicons name="file-tray-full-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="profile"
|
||||
options={{
|
||||
title: 'Profil',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Ionicons name="person-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
// 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 (
|
||||
<Drawer
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
drawerStyle: {
|
||||
backgroundColor: drawerStyles.backgroundColor,
|
||||
},
|
||||
drawerActiveTintColor: drawerStyles.contentOptions.activeTintColor,
|
||||
drawerInactiveTintColor: drawerStyles.contentOptions.inactiveTintColor,
|
||||
drawerActiveBackgroundColor: drawerStyles.contentOptions.activeBackgroundColor,
|
||||
}}
|
||||
>
|
||||
<Drawer.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Chat',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Ionicons name="chatbubbles-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="documents"
|
||||
options={{
|
||||
title: 'Dokumente',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Ionicons name="document-text-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="archive"
|
||||
options={{
|
||||
title: 'Archiv',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Ionicons name="archive-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="templates"
|
||||
options={{
|
||||
title: 'Vorlagen',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Ionicons name="file-tray-full-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="profile"
|
||||
options={{
|
||||
title: 'Profil',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Ionicons name="person-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,34 +5,34 @@ import { ScrollViewStyleReset } from 'expo-router/html';
|
|||
// 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 (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
|
||||
{/*
|
||||
{/*
|
||||
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:
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
*/}
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1.00001,viewport-fit=cover"
|
||||
/>
|
||||
{/*
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1.00001,viewport-fit=cover"
|
||||
/>
|
||||
{/*
|
||||
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.
|
||||
*/}
|
||||
<ScrollViewStyleReset />
|
||||
<ScrollViewStyleReset />
|
||||
|
||||
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
|
||||
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
|
||||
{/* Add any additional <head> elements that you want globally available on web... */}
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
|
||||
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
|
||||
{/* Add any additional <head> elements that you want globally available on web... */}
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
const responsiveBackground = `
|
||||
|
|
|
|||
|
|
@ -4,21 +4,21 @@ import { Text } from 'react-native';
|
|||
import { Container } from '~/components/Container';
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: 'Oops!' }} />
|
||||
<Container>
|
||||
<Text className={styles.title}>This screen doesn't exist.</Text>
|
||||
<Link href="/" className={styles.link}>
|
||||
<Text className={styles.linkText}>Go to home screen!</Text>
|
||||
</Link>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: 'Oops!' }} />
|
||||
<Container>
|
||||
<Text className={styles.title}>This screen doesn't exist.</Text>
|
||||
<Link href="/" className={styles.link}>
|
||||
<Text className={styles.linkText}>Go to home screen!</Text>
|
||||
</Link>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = {
|
||||
title: `text-xl font-bold`,
|
||||
link: `mt-4 pt-4`,
|
||||
linkText: `text-base text-[#2e78b7]`,
|
||||
title: `text-xl font-bold`,
|
||||
link: `mt-4 pt-4`,
|
||||
linkText: `text-base text-[#2e78b7]`,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,64 +9,64 @@ 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)',
|
||||
// Ensure that reloading on `/modal` keeps a back button present.
|
||||
initialRouteName: '(drawer)',
|
||||
};
|
||||
|
||||
function Layout() {
|
||||
const { theme } = useAppTheme();
|
||||
|
||||
return (
|
||||
<NavigationThemeProvider value={theme}>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ title: 'Modal', presentation: 'modal' }} />
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="model-selection" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="templates" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="conversation/[id]" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="auth/register" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="auth/reset-password" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="profile" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</GestureHandlerRootView>
|
||||
</NavigationThemeProvider>
|
||||
);
|
||||
const { theme } = useAppTheme();
|
||||
|
||||
return (
|
||||
<NavigationThemeProvider value={theme}>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ title: 'Modal', presentation: 'modal' }} />
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="model-selection" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="templates" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="conversation/[id]" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="auth/register" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="auth/reset-password" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="profile" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</GestureHandlerRootView>
|
||||
</NavigationThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Authentifizierungsprüfung und Umleitung
|
||||
function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
const { user, loading } = useAuth();
|
||||
const segments = useSegments();
|
||||
const router = useRouter();
|
||||
const { user, loading } = useAuth();
|
||||
const segments = useSegments();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
|
||||
const inAuthGroup = segments[0] === 'auth';
|
||||
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]);
|
||||
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}</>;
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<AuthGuard>
|
||||
<Layout />
|
||||
</AuthGuard>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<AuthGuard>
|
||||
<Layout />
|
||||
</AuthGuard>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,97 +2,100 @@ const BACKEND_URL = process.env.EXPO_PUBLIC_BACKEND_URL || 'http://localhost:300
|
|||
|
||||
// Definiere den Typ für ein Modell
|
||||
export type Model = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
parameters?: Record<string, any>;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
parameters?: Record<string, any>;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
// Fallback-Modelle, falls keine aus dem Backend 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'
|
||||
}
|
||||
}
|
||||
{
|
||||
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 vom Backend zu laden
|
||||
let models: Model[] = FALLBACK_MODELS;
|
||||
try {
|
||||
// Versuche, Modelle vom Backend zu laden
|
||||
let models: Model[] = FALLBACK_MODELS;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/chat/models`);
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/chat/models`);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data && data.length > 0) {
|
||||
models = data as Model[];
|
||||
}
|
||||
} else {
|
||||
console.error('Fehler beim Laden der Modelle vom Backend:', response.status);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fehler bei der Backend-Verbindung:', e);
|
||||
// Fallback zu den vordefinierten Modellen
|
||||
}
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data && data.length > 0) {
|
||||
models = data as Model[];
|
||||
}
|
||||
} else {
|
||||
console.error('Fehler beim Laden der Modelle vom Backend:', response.status);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fehler bei der Backend-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',
|
||||
},
|
||||
});
|
||||
}
|
||||
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 (nicht unterstützt ohne Backend-Endpoint)
|
||||
export async function POST(request: Request) {
|
||||
return new Response(JSON.stringify({ error: 'Modell-Erstellung wird über das Backend nicht unterstützt' }), {
|
||||
status: 501,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Modell-Erstellung wird über das Backend nicht unterstützt' }),
|
||||
{
|
||||
status: 501,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,99 +3,99 @@
|
|||
|
||||
// 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;
|
||||
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;
|
||||
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;
|
||||
message_id: string;
|
||||
created_at: string;
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
estimated_cost: number;
|
||||
};
|
||||
|
||||
// Handler für GET /api/usage
|
||||
// TODO: Backend-Endpoints für Usage-Statistiken implementieren
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const userId = url.searchParams.get('userId');
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const userId = url.searchParams.get('userId');
|
||||
|
||||
if (!userId) {
|
||||
return new Response(JSON.stringify({ error: 'User ID ist erforderlich' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
if (!userId) {
|
||||
return new Response(JSON.stringify({ error: 'User ID ist erforderlich' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// Usage-Statistiken sind noch nicht über die Backend-API verfügbar
|
||||
// Gebe leere Daten zurück
|
||||
console.log('Usage-Statistiken: Backend-Endpoints noch nicht implementiert');
|
||||
// Usage-Statistiken sind noch nicht über die Backend-API verfügbar
|
||||
// Gebe leere Daten zurück
|
||||
console.log('Usage-Statistiken: Backend-Endpoints noch nicht implementiert');
|
||||
|
||||
return Response.json({
|
||||
modelUsage: [],
|
||||
periodUsage: [],
|
||||
summary: {
|
||||
totalCost: 0,
|
||||
totalTokens: 0
|
||||
},
|
||||
message: 'Usage-Statistiken sind derzeit nicht verfügbar'
|
||||
});
|
||||
} 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' }
|
||||
});
|
||||
}
|
||||
return Response.json({
|
||||
modelUsage: [],
|
||||
periodUsage: [],
|
||||
summary: {
|
||||
totalCost: 0,
|
||||
totalTokens: 0,
|
||||
},
|
||||
message: 'Usage-Statistiken sind derzeit nicht verfügbar',
|
||||
});
|
||||
} 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
|
||||
// TODO: Backend-Endpoints für Conversation-Usage implementieren
|
||||
export async function GET_conversation(request: Request) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const conversationId = url.searchParams.get('conversationId');
|
||||
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' }
|
||||
});
|
||||
}
|
||||
if (!conversationId) {
|
||||
return new Response(JSON.stringify({ error: 'Conversation ID ist erforderlich' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// Usage-Statistiken sind noch nicht über die Backend-API verfügbar
|
||||
// Gebe leere Daten zurück
|
||||
console.log('Conversation-Usage: Backend-Endpoints noch nicht implementiert');
|
||||
// Usage-Statistiken sind noch nicht über die Backend-API verfügbar
|
||||
// Gebe leere Daten zurück
|
||||
console.log('Conversation-Usage: Backend-Endpoints noch nicht implementiert');
|
||||
|
||||
return Response.json({
|
||||
conversationUsage: [],
|
||||
summary: {
|
||||
totalCost: 0,
|
||||
totalTokens: 0,
|
||||
messageCount: 0
|
||||
},
|
||||
message: 'Usage-Statistiken sind derzeit nicht verfügbar'
|
||||
});
|
||||
} 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' }
|
||||
});
|
||||
}
|
||||
return Response.json({
|
||||
conversationUsage: [],
|
||||
summary: {
|
||||
totalCost: 0,
|
||||
totalTokens: 0,
|
||||
messageCount: 0,
|
||||
},
|
||||
message: 'Usage-Statistiken sind derzeit nicht verfügbar',
|
||||
});
|
||||
} 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' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
SafeAreaView,
|
||||
Alert,
|
||||
ActivityIndicator
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
SafeAreaView,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { useTheme, useFocusEffect } from '@react-navigation/native';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
|
@ -16,488 +16,481 @@ import { useAuth } from '../context/AuthProvider';
|
|||
import { useAppTheme } from '../theme/ThemeProvider';
|
||||
import CustomDrawer from '../components/CustomDrawer';
|
||||
import {
|
||||
getArchivedConversations,
|
||||
getMessages,
|
||||
deleteConversation,
|
||||
unarchiveConversation
|
||||
getArchivedConversations,
|
||||
getMessages,
|
||||
deleteConversation,
|
||||
unarchiveConversation,
|
||||
} from '../services/conversation';
|
||||
import { modelApi } from '../services/api';
|
||||
|
||||
// Typendefinitionen für Konversationen
|
||||
type ConversationItem = {
|
||||
id: string;
|
||||
modelName: string;
|
||||
title: string;
|
||||
lastMessage: string;
|
||||
timestamp: Date;
|
||||
mode: 'frei' | 'geführt' | 'vorlage';
|
||||
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}`;
|
||||
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<ConversationItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { isDarkMode } = useAppTheme();
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const { colors } = useTheme();
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const [conversations, setConversations] = useState<ConversationItem[]>([]);
|
||||
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 über die Backend API
|
||||
const modelData = await modelApi.getModel(conv.model_id);
|
||||
// Eine Funktion, die Konversationen lädt und wiederverwendet werden kann
|
||||
const loadConversations = async () => {
|
||||
if (!user) return;
|
||||
|
||||
// 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];
|
||||
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()
|
||||
);
|
||||
|
||||
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])
|
||||
);
|
||||
// Lade für jede Konversation die letzte Nachricht und das Modell
|
||||
const conversationItems: ConversationItem[] = [];
|
||||
|
||||
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.");
|
||||
}
|
||||
};
|
||||
for (const conv of userConversations) {
|
||||
try {
|
||||
// Lade die Nachrichten der Konversation
|
||||
const messages = await getMessages(conv.id);
|
||||
// Lade das Modell über die Backend API
|
||||
const modelData = await modelApi.getModel(conv.model_id);
|
||||
|
||||
// Zustandsverwaltung für die Optionsmenüs der Konversationselemente
|
||||
const [expandedConversationId, setExpandedConversationId] = useState<string | null>(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 (
|
||||
<View style={[styles.conversationItemWrapper, { backgroundColor: colors.card }]}>
|
||||
<TouchableOpacity
|
||||
style={styles.conversationItem}
|
||||
onPress={() => handleConversationPress(item.id)}
|
||||
onLongPress={() => toggleOptionsMenu(item.id)}
|
||||
>
|
||||
<View style={styles.conversationContent}>
|
||||
<View style={styles.conversationHeader}>
|
||||
<View style={styles.titleRow}>
|
||||
<Ionicons
|
||||
name="archive-outline"
|
||||
size={18}
|
||||
color={colors.text}
|
||||
style={styles.titleIcon}
|
||||
/>
|
||||
<Text style={[styles.title, { color: colors.text }]} numberOfLines={1}>
|
||||
{item.title}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[styles.timestamp, { color: colors.text + '80' }]}>
|
||||
{formatDate(item.timestamp)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.modelContainer}>
|
||||
<Text style={[styles.modelName, { color: colors.text + 'AA' }]}>
|
||||
{item.modelName}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[styles.lastMessage, { color: colors.text + 'CC' }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.lastMessage}
|
||||
</Text>
|
||||
|
||||
<View style={styles.modeContainer}>
|
||||
<Text style={[styles.modeText, { color: colors.text + '80' }]}>
|
||||
{item.mode === 'frei' ? 'Freier Modus' :
|
||||
item.mode === 'geführt' ? 'Geführter Modus' : 'Vorlagen-Modus'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity onPress={() => toggleOptionsMenu(item.id)}>
|
||||
<Ionicons name="ellipsis-vertical" size={20} color={colors.text + '80'} />
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
|
||||
{showOptions && (
|
||||
<View style={[styles.optionsContainer, { backgroundColor: colors.card }]}>
|
||||
<TouchableOpacity
|
||||
style={styles.optionButton}
|
||||
onPress={() => handleUnarchiveConversation(item.id)}
|
||||
>
|
||||
<Ionicons name="arrow-undo-outline" size={18} color={colors.text} />
|
||||
<Text style={[styles.optionText, { color: colors.text }]}>Wiederherstellen</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.optionButton}
|
||||
onPress={() => handleDeleteConversation(item.id)}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
|
||||
<Text style={[styles.optionText, { color: "#FF3B30" }]}>Löschen</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
// 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];
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.mainLayout}>
|
||||
{/* Permanenter Drawer links */}
|
||||
{isDrawerOpen && (
|
||||
<View style={styles.drawerContainer}>
|
||||
<CustomDrawer
|
||||
isVisible={isDrawerOpen}
|
||||
onClose={() => setIsDrawerOpen(false)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Hauptinhalt */}
|
||||
<View style={styles.mainContainer}>
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.headerContainer}>
|
||||
<TouchableOpacity
|
||||
style={styles.menuButton}
|
||||
onPress={() => setIsDrawerOpen(!isDrawerOpen)}
|
||||
>
|
||||
<Ionicons
|
||||
name="menu-outline"
|
||||
size={28}
|
||||
color={colors.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.headerContentContainer}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.title, { color: colors.text }]}>Archiv</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Konversationsliste */}
|
||||
<View style={styles.listContainer}>
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: colors.text + '80' }]}>
|
||||
Konversationen werden geladen...
|
||||
</Text>
|
||||
</View>
|
||||
) : conversations.length > 0 ? (
|
||||
<FlatList
|
||||
data={conversations}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderConversationItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons
|
||||
name="archive-outline"
|
||||
size={64}
|
||||
color={colors.text + '40'}
|
||||
/>
|
||||
<Text style={[styles.emptyText, { color: colors.text + '80' }]}>
|
||||
Keine archivierten Konversationen
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtext, { color: colors.text + '60' }]}>
|
||||
Archivierte Gespräche erscheinen hier
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
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<string | null>(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 (
|
||||
<View style={[styles.conversationItemWrapper, { backgroundColor: colors.card }]}>
|
||||
<TouchableOpacity
|
||||
style={styles.conversationItem}
|
||||
onPress={() => handleConversationPress(item.id)}
|
||||
onLongPress={() => toggleOptionsMenu(item.id)}
|
||||
>
|
||||
<View style={styles.conversationContent}>
|
||||
<View style={styles.conversationHeader}>
|
||||
<View style={styles.titleRow}>
|
||||
<Ionicons
|
||||
name="archive-outline"
|
||||
size={18}
|
||||
color={colors.text}
|
||||
style={styles.titleIcon}
|
||||
/>
|
||||
<Text style={[styles.title, { color: colors.text }]} numberOfLines={1}>
|
||||
{item.title}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[styles.timestamp, { color: colors.text + '80' }]}>
|
||||
{formatDate(item.timestamp)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.modelContainer}>
|
||||
<Text style={[styles.modelName, { color: colors.text + 'AA' }]}>
|
||||
{item.modelName}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.lastMessage, { color: colors.text + 'CC' }]} numberOfLines={1}>
|
||||
{item.lastMessage}
|
||||
</Text>
|
||||
|
||||
<View style={styles.modeContainer}>
|
||||
<Text style={[styles.modeText, { color: colors.text + '80' }]}>
|
||||
{item.mode === 'frei'
|
||||
? 'Freier Modus'
|
||||
: item.mode === 'geführt'
|
||||
? 'Geführter Modus'
|
||||
: 'Vorlagen-Modus'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity onPress={() => toggleOptionsMenu(item.id)}>
|
||||
<Ionicons name="ellipsis-vertical" size={20} color={colors.text + '80'} />
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
|
||||
{showOptions && (
|
||||
<View style={[styles.optionsContainer, { backgroundColor: colors.card }]}>
|
||||
<TouchableOpacity
|
||||
style={styles.optionButton}
|
||||
onPress={() => handleUnarchiveConversation(item.id)}
|
||||
>
|
||||
<Ionicons name="arrow-undo-outline" size={18} color={colors.text} />
|
||||
<Text style={[styles.optionText, { color: colors.text }]}>Wiederherstellen</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.optionButton}
|
||||
onPress={() => handleDeleteConversation(item.id)}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
|
||||
<Text style={[styles.optionText, { color: '#FF3B30' }]}>Löschen</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.mainLayout}>
|
||||
{/* Permanenter Drawer links */}
|
||||
{isDrawerOpen && (
|
||||
<View style={styles.drawerContainer}>
|
||||
<CustomDrawer isVisible={isDrawerOpen} onClose={() => setIsDrawerOpen(false)} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Hauptinhalt */}
|
||||
<View style={styles.mainContainer}>
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.headerContainer}>
|
||||
<TouchableOpacity
|
||||
style={styles.menuButton}
|
||||
onPress={() => setIsDrawerOpen(!isDrawerOpen)}
|
||||
>
|
||||
<Ionicons name="menu-outline" size={28} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.headerContentContainer}>
|
||||
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
||||
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.title, { color: colors.text }]}>Archiv</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Konversationsliste */}
|
||||
<View style={styles.listContainer}>
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: colors.text + '80' }]}>
|
||||
Konversationen werden geladen...
|
||||
</Text>
|
||||
</View>
|
||||
) : conversations.length > 0 ? (
|
||||
<FlatList
|
||||
data={conversations}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderConversationItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="archive-outline" size={64} color={colors.text + '40'} />
|
||||
<Text style={[styles.emptyText, { color: colors.text + '80' }]}>
|
||||
Keine archivierten Konversationen
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtext, { color: colors.text + '60' }]}>
|
||||
Archivierte Gespräche erscheinen hier
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,5 @@ import React from 'react';
|
|||
import { Stack } from 'expo-router';
|
||||
|
||||
export default function AuthLayout() {
|
||||
return (
|
||||
<Stack screenOptions={{ headerShown: false }} />
|
||||
);
|
||||
return <Stack screenOptions={{ headerShown: false }} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator, Alert } from 'react-native';
|
||||
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';
|
||||
|
|
@ -7,252 +15,255 @@ import { useAuth } from '../../context/AuthProvider';
|
|||
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 { colors } = useTheme();
|
||||
const { isDarkMode } = useAppTheme();
|
||||
const router = useRouter();
|
||||
const { signIn } = useAuth();
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!email || !password) {
|
||||
Alert.alert('Fehler', 'Bitte gib deine E-Mail-Adresse und dein Passwort ein.');
|
||||
return;
|
||||
}
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isMagicLinkSent, setIsMagicLinkSent] = useState(false);
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const { error } = await signIn(email, password);
|
||||
const handleLogin = async () => {
|
||||
if (!email || !password) {
|
||||
Alert.alert('Fehler', 'Bitte gib deine E-Mail-Adresse und dein Passwort ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
Alert.alert('Anmeldung fehlgeschlagen', error.message || 'Unbekannter Fehler');
|
||||
} 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);
|
||||
}
|
||||
};
|
||||
|
||||
// Magic Link ist derzeit nicht verfügbar (mana-core-auth unterstützt dies nicht)
|
||||
const handleMagicLink = async () => {
|
||||
Alert.alert(
|
||||
'Nicht verfügbar',
|
||||
'Magic Link Anmeldung ist derzeit nicht verfügbar. Bitte nutze E-Mail und Passwort.'
|
||||
);
|
||||
};
|
||||
try {
|
||||
setLoading(true);
|
||||
const { error } = await signIn(email, password);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colors.text }]}>Willkommen zurück</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.text + 'CC' }]}>
|
||||
Melde dich an, um deine Konversationen fortzusetzen
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={[styles.label, { color: colors.text }]}>E-Mail</Text>
|
||||
<View style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: isDarkMode ? '#3A3A3C' : '#E5E5EA'
|
||||
}
|
||||
]}>
|
||||
<Ionicons name="mail-outline" size={20} color={colors.text + '80'} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholder="deine@email.de"
|
||||
placeholderTextColor={colors.text + '60'}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={[styles.label, { color: colors.text }]}>Passwort</Text>
|
||||
<View style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: isDarkMode ? '#3A3A3C' : '#E5E5EA'
|
||||
}
|
||||
]}>
|
||||
<Ionicons name="lock-closed-outline" size={20} color={colors.text + '80'} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholder="Passwort"
|
||||
placeholderTextColor={colors.text + '60'}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry={!showPassword}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setShowPassword(!showPassword)}>
|
||||
<Ionicons
|
||||
name={showPassword ? "eye-off-outline" : "eye-outline"}
|
||||
size={20}
|
||||
color={colors.text + '80'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.forgotPassword}
|
||||
onPress={() => router.push('/auth/reset-password')}
|
||||
>
|
||||
<Text style={[styles.forgotPasswordText, { color: colors.primary }]}>
|
||||
Passwort vergessen?
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.loginButton,
|
||||
{ backgroundColor: colors.primary },
|
||||
loading && { opacity: 0.7 }
|
||||
]}
|
||||
onPress={handleLogin}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#FFFFFF" size="small" />
|
||||
) : (
|
||||
<Text style={styles.loginButtonText}>Anmelden</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.magicLinkButton,
|
||||
{ backgroundColor: 'transparent', borderColor: colors.primary, borderWidth: 1 },
|
||||
loading && { opacity: 0.7 }
|
||||
]}
|
||||
onPress={handleMagicLink}
|
||||
disabled={loading || isMagicLinkSent}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color={colors.primary} size="small" />
|
||||
) : (
|
||||
<Text style={[styles.magicLinkButtonText, { color: colors.primary }]}>
|
||||
{isMagicLinkSent ? 'Magic Link gesendet' : 'Mit Magic Link anmelden'}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.signupContainer}>
|
||||
<Text style={[styles.signupText, { color: colors.text + 'CC' }]}>
|
||||
Noch kein Konto?
|
||||
</Text>
|
||||
<Link href="/auth/register" asChild>
|
||||
<TouchableOpacity>
|
||||
<Text style={[styles.signupLink, { color: colors.primary }]}>
|
||||
Registrieren
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
if (error) {
|
||||
Alert.alert('Anmeldung fehlgeschlagen', error.message || 'Unbekannter Fehler');
|
||||
} 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);
|
||||
}
|
||||
};
|
||||
|
||||
// Magic Link ist derzeit nicht verfügbar (mana-core-auth unterstützt dies nicht)
|
||||
const handleMagicLink = async () => {
|
||||
Alert.alert(
|
||||
'Nicht verfügbar',
|
||||
'Magic Link Anmeldung ist derzeit nicht verfügbar. Bitte nutze E-Mail und Passwort.'
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colors.text }]}>Willkommen zurück</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.text + 'CC' }]}>
|
||||
Melde dich an, um deine Konversationen fortzusetzen
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={[styles.label, { color: colors.text }]}>E-Mail</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: isDarkMode ? '#3A3A3C' : '#E5E5EA',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons name="mail-outline" size={20} color={colors.text + '80'} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholder="deine@email.de"
|
||||
placeholderTextColor={colors.text + '60'}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={[styles.label, { color: colors.text }]}>Passwort</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: isDarkMode ? '#3A3A3C' : '#E5E5EA',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons name="lock-closed-outline" size={20} color={colors.text + '80'} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholder="Passwort"
|
||||
placeholderTextColor={colors.text + '60'}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry={!showPassword}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setShowPassword(!showPassword)}>
|
||||
<Ionicons
|
||||
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
|
||||
size={20}
|
||||
color={colors.text + '80'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.forgotPassword}
|
||||
onPress={() => router.push('/auth/reset-password')}
|
||||
>
|
||||
<Text style={[styles.forgotPasswordText, { color: colors.primary }]}>
|
||||
Passwort vergessen?
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.loginButton,
|
||||
{ backgroundColor: colors.primary },
|
||||
loading && { opacity: 0.7 },
|
||||
]}
|
||||
onPress={handleLogin}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#FFFFFF" size="small" />
|
||||
) : (
|
||||
<Text style={styles.loginButtonText}>Anmelden</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.magicLinkButton,
|
||||
{ backgroundColor: 'transparent', borderColor: colors.primary, borderWidth: 1 },
|
||||
loading && { opacity: 0.7 },
|
||||
]}
|
||||
onPress={handleMagicLink}
|
||||
disabled={loading || isMagicLinkSent}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color={colors.primary} size="small" />
|
||||
) : (
|
||||
<Text style={[styles.magicLinkButtonText, { color: colors.primary }]}>
|
||||
{isMagicLinkSent ? 'Magic Link gesendet' : 'Mit Magic Link anmelden'}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.signupContainer}>
|
||||
<Text style={[styles.signupText, { color: colors.text + 'CC' }]}>Noch kein Konto?</Text>
|
||||
<Link href="/auth/register" asChild>
|
||||
<TouchableOpacity>
|
||||
<Text style={[styles.signupLink, { color: colors.primary }]}>Registrieren</Text>
|
||||
</TouchableOpacity>
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator, Alert } from 'react-native';
|
||||
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';
|
||||
|
|
@ -7,238 +15,243 @@ 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 { colors } = useTheme();
|
||||
const { isDarkMode } = useAppTheme();
|
||||
const router = useRouter();
|
||||
const { signUp } = useAuth();
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colors.text }]}>Konto erstellen</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.text + 'CC' }]}>
|
||||
Erstelle ein Konto, um mit KI-Modellen zu chatten
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={[styles.label, { color: colors.text }]}>E-Mail</Text>
|
||||
<View style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: isDarkMode ? '#3A3A3C' : '#E5E5EA'
|
||||
}
|
||||
]}>
|
||||
<Ionicons name="mail-outline" size={20} color={colors.text + '80'} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholder="deine@email.de"
|
||||
placeholderTextColor={colors.text + '60'}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={[styles.label, { color: colors.text }]}>Passwort</Text>
|
||||
<View style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: isDarkMode ? '#3A3A3C' : '#E5E5EA'
|
||||
}
|
||||
]}>
|
||||
<Ionicons name="lock-closed-outline" size={20} color={colors.text + '80'} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholder="Passwort"
|
||||
placeholderTextColor={colors.text + '60'}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry={!showPassword}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setShowPassword(!showPassword)}>
|
||||
<Ionicons
|
||||
name={showPassword ? "eye-off-outline" : "eye-outline"}
|
||||
size={20}
|
||||
color={colors.text + '80'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={[styles.label, { color: colors.text }]}>Passwort bestätigen</Text>
|
||||
<View style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: isDarkMode ? '#3A3A3C' : '#E5E5EA'
|
||||
}
|
||||
]}>
|
||||
<Ionicons name="lock-closed-outline" size={20} color={colors.text + '80'} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholder="Passwort bestätigen"
|
||||
placeholderTextColor={colors.text + '60'}
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
secureTextEntry={!showPassword}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.registerButton,
|
||||
{ backgroundColor: colors.primary },
|
||||
loading && { opacity: 0.7 }
|
||||
]}
|
||||
onPress={handleRegister}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#FFFFFF" size="small" />
|
||||
) : (
|
||||
<Text style={styles.registerButtonText}>Registrieren</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.loginContainer}>
|
||||
<Text style={[styles.loginText, { color: colors.text + 'CC' }]}>
|
||||
Bereits ein Konto?
|
||||
</Text>
|
||||
<Link href="/auth/login" asChild>
|
||||
<TouchableOpacity>
|
||||
<Text style={[styles.loginLink, { color: colors.primary }]}>
|
||||
Anmelden
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
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 (
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colors.text }]}>Konto erstellen</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.text + 'CC' }]}>
|
||||
Erstelle ein Konto, um mit KI-Modellen zu chatten
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={[styles.label, { color: colors.text }]}>E-Mail</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: isDarkMode ? '#3A3A3C' : '#E5E5EA',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons name="mail-outline" size={20} color={colors.text + '80'} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholder="deine@email.de"
|
||||
placeholderTextColor={colors.text + '60'}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={[styles.label, { color: colors.text }]}>Passwort</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: isDarkMode ? '#3A3A3C' : '#E5E5EA',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons name="lock-closed-outline" size={20} color={colors.text + '80'} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholder="Passwort"
|
||||
placeholderTextColor={colors.text + '60'}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry={!showPassword}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setShowPassword(!showPassword)}>
|
||||
<Ionicons
|
||||
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
|
||||
size={20}
|
||||
color={colors.text + '80'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={[styles.label, { color: colors.text }]}>Passwort bestätigen</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: isDarkMode ? '#3A3A3C' : '#E5E5EA',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons name="lock-closed-outline" size={20} color={colors.text + '80'} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholder="Passwort bestätigen"
|
||||
placeholderTextColor={colors.text + '60'}
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
secureTextEntry={!showPassword}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.registerButton,
|
||||
{ backgroundColor: colors.primary },
|
||||
loading && { opacity: 0.7 },
|
||||
]}
|
||||
onPress={handleRegister}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#FFFFFF" size="small" />
|
||||
) : (
|
||||
<Text style={styles.registerButtonText}>Registrieren</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.loginContainer}>
|
||||
<Text style={[styles.loginText, { color: colors.text + 'CC' }]}>Bereits ein Konto?</Text>
|
||||
<Link href="/auth/login" asChild>
|
||||
<TouchableOpacity>
|
||||
<Text style={[styles.loginLink, { color: colors.primary }]}>Anmelden</Text>
|
||||
</TouchableOpacity>
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator, Alert } from 'react-native';
|
||||
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';
|
||||
|
|
@ -7,166 +15,166 @@ 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 { colors } = useTheme();
|
||||
const { isDarkMode } = useAppTheme();
|
||||
const router = useRouter();
|
||||
const { resetPassword } = useAuth();
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
const [email, setEmail] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colors.text }]}>Passwort zurücksetzen</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.text + 'CC' }]}>
|
||||
Gib deine E-Mail-Adresse ein, um einen Link zum Zurücksetzen deines Passworts zu erhalten
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={[styles.label, { color: colors.text }]}>E-Mail</Text>
|
||||
<View style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: isDarkMode ? '#3A3A3C' : '#E5E5EA'
|
||||
}
|
||||
]}>
|
||||
<Ionicons name="mail-outline" size={20} color={colors.text + '80'} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholder="deine@email.de"
|
||||
placeholderTextColor={colors.text + '60'}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.resetButton,
|
||||
{ backgroundColor: colors.primary },
|
||||
loading && { opacity: 0.7 }
|
||||
]}
|
||||
onPress={handleResetPassword}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#FFFFFF" size="small" />
|
||||
) : (
|
||||
<Text style={styles.resetButtonText}>Link senden</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Text style={[styles.backButtonText, { color: colors.text }]}>
|
||||
Zurück zur Anmeldung
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
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 (
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colors.text }]}>Passwort zurücksetzen</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.text + 'CC' }]}>
|
||||
Gib deine E-Mail-Adresse ein, um einen Link zum Zurücksetzen deines Passworts zu erhalten
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={[styles.label, { color: colors.text }]}>E-Mail</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: isDarkMode ? '#3A3A3C' : '#E5E5EA',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons name="mail-outline" size={20} color={colors.text + '80'} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholder="deine@email.de"
|
||||
placeholderTextColor={colors.text + '60'}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.resetButton,
|
||||
{ backgroundColor: colors.primary },
|
||||
loading && { opacity: 0.7 },
|
||||
]}
|
||||
onPress={handleResetPassword}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#FFFFFF" size="small" />
|
||||
) : (
|
||||
<Text style={styles.resetButtonText}>Link senden</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
||||
<Text style={[styles.backButtonText, { color: colors.text }]}>Zurück zur Anmeldung</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -7,123 +7,119 @@ 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
|
||||
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<ConversationNewParams>();
|
||||
const [isFetching, setIsFetching] = useState(true);
|
||||
const { user } = useAuth();
|
||||
const router = useRouter();
|
||||
const params = useLocalSearchParams<ConversationNewParams>();
|
||||
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");
|
||||
// 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;
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
console.error('Kein Benutzer gefunden');
|
||||
router.replace('/auth/login');
|
||||
return;
|
||||
}
|
||||
console.log('Erhaltene Parameter:', {
|
||||
initialMessage: initialMessage.substring(0, 50),
|
||||
modelId,
|
||||
templateId,
|
||||
mode,
|
||||
documentMode,
|
||||
spaceId: spaceId || 'nicht angegeben',
|
||||
});
|
||||
|
||||
if (!initialMessage) {
|
||||
console.warn('Keine Nachricht gefunden');
|
||||
router.replace('/');
|
||||
return;
|
||||
}
|
||||
// Log für Debug-Zwecke
|
||||
console.log('⭐️ Neue Konversation wird erstellt mit Space ID:', spaceId || 'keine');
|
||||
|
||||
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
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
console.error('Kein Benutzer gefunden');
|
||||
router.replace('/auth/login');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!conversationId) {
|
||||
throw new Error('Fehler beim Erstellen der Konversation');
|
||||
}
|
||||
if (!initialMessage) {
|
||||
console.warn('Keine Nachricht gefunden');
|
||||
router.replace('/');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Konversation erstellt mit ID:', conversationId);
|
||||
|
||||
// 2. Sende die initiale Nachricht
|
||||
const response = await sendMessageAndGetResponse(
|
||||
conversationId,
|
||||
initialMessage,
|
||||
modelId,
|
||||
templateId,
|
||||
documentMode
|
||||
);
|
||||
const startConversation = async () => {
|
||||
try {
|
||||
setIsFetching(true);
|
||||
console.log('Erstelle Konversation...');
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
// 1. Erstelle eine neue Konversation
|
||||
const conversationId = await createConversation(
|
||||
user.id,
|
||||
modelId,
|
||||
mode,
|
||||
templateId,
|
||||
documentMode,
|
||||
spaceId
|
||||
);
|
||||
|
||||
startConversation();
|
||||
}, [user, initialMessage, modelId, templateId, mode, documentMode, spaceId, router]);
|
||||
if (!conversationId) {
|
||||
throw new Error('Fehler beim Erstellen der Konversation');
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ActivityIndicator size="large" color="#0000ff" />
|
||||
<Text style={styles.text}>Starte Konversation...</Text>
|
||||
</View>
|
||||
);
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
<ActivityIndicator size="large" color="#0000ff" />
|
||||
<Text style={styles.text}>Starte Konversation...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
marginTop: 20,
|
||||
fontSize: 16,
|
||||
}
|
||||
});
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
marginTop: 20,
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,13 +1,13 @@
|
|||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
useWindowDimensions,
|
||||
Platform
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
useWindowDimensions,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { useTheme } from '@react-navigation/native';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
|
@ -18,419 +18,428 @@ import { useAuth } from '../context/AuthProvider';
|
|||
import Markdown from 'react-native-markdown-display';
|
||||
|
||||
type DocumentWithTitle = Document & {
|
||||
conversation_title: string;
|
||||
conversation_title: string;
|
||||
};
|
||||
|
||||
export default function DocumentsScreen() {
|
||||
const { colors } = useTheme();
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { width } = useWindowDimensions();
|
||||
const [documents, setDocuments] = useState<DocumentWithTitle[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { colors } = useTheme();
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { width } = useWindowDimensions();
|
||||
const [documents, setDocuments] = useState<DocumentWithTitle[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// 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 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;
|
||||
// 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
|
||||
// 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]);
|
||||
return availableWidth * widthRatio;
|
||||
}, [width, columnsCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.id) {
|
||||
loadDocuments();
|
||||
}
|
||||
}, [user]);
|
||||
useEffect(() => {
|
||||
if (user?.id) {
|
||||
loadDocuments();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const loadDocuments = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const loadDocuments = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Lade alle Konversationen des Benutzers über die Backend-API
|
||||
const conversations = await conversationApi.getConversations();
|
||||
// Lade alle Konversationen des Benutzers über die Backend-API
|
||||
const conversations = await conversationApi.getConversations();
|
||||
|
||||
// Filtere nur Konversationen im Dokumentmodus
|
||||
const documentConversations = conversations.filter(conv => conv.documentMode);
|
||||
// Filtere nur Konversationen im Dokumentmodus
|
||||
const documentConversations = conversations.filter((conv) => conv.documentMode);
|
||||
|
||||
if (documentConversations.length === 0) {
|
||||
setDocuments([]);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (documentConversations.length === 0) {
|
||||
setDocuments([]);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Für jede Konversation den neuesten Dokumentstand laden
|
||||
const latestDocuments: DocumentWithTitle[] = [];
|
||||
// Für jede Konversation den neuesten Dokumentstand laden
|
||||
const latestDocuments: DocumentWithTitle[] = [];
|
||||
|
||||
for (const conv of documentConversations) {
|
||||
try {
|
||||
const docData = await getLatestDocument(conv.id);
|
||||
for (const conv of documentConversations) {
|
||||
try {
|
||||
const docData = await getLatestDocument(conv.id);
|
||||
|
||||
if (docData) {
|
||||
latestDocuments.push({
|
||||
...docData,
|
||||
conversation_title: conv.title || 'Unbenannte Konversation'
|
||||
});
|
||||
}
|
||||
} catch (docError) {
|
||||
console.error(`Fehler beim Laden des Dokuments für Konversation ${conv.id}:`, docError);
|
||||
}
|
||||
}
|
||||
if (docData) {
|
||||
latestDocuments.push({
|
||||
...docData,
|
||||
conversation_title: conv.title || 'Unbenannte Konversation',
|
||||
});
|
||||
}
|
||||
} catch (docError) {
|
||||
console.error(`Fehler beim Laden des Dokuments für Konversation ${conv.id}:`, docError);
|
||||
}
|
||||
}
|
||||
|
||||
setDocuments(latestDocuments);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Dokumente:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
setDocuments(latestDocuments);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Dokumente:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToConversation = (conversationId: string) => {
|
||||
router.push(`/conversation/${conversationId}`);
|
||||
};
|
||||
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}`;
|
||||
};
|
||||
// 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();
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: colors.text }]}>Alle Dokumente</Text>
|
||||
<TouchableOpacity style={styles.refreshButton} onPress={loadDocuments}>
|
||||
<Ionicons name="refresh" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: colors.text }]}>
|
||||
Dokumente werden geladen...
|
||||
</Text>
|
||||
</View>
|
||||
) : documents.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="document-text-outline" size={64} color={colors.text} style={styles.emptyIcon} />
|
||||
<Text style={[styles.emptyText, { color: colors.text }]}>
|
||||
Keine Dokumente gefunden
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtext, { color: colors.text }]}>
|
||||
Erstelle ein neues Dokument in einer Konversation mit aktiviertem Dokumentmodus.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView style={styles.scrollContainer} contentContainerStyle={styles.documentsContainer}>
|
||||
{documents.map((doc) => (
|
||||
<TouchableOpacity
|
||||
key={doc.id}
|
||||
style={[
|
||||
styles.documentCard,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
width: cardWidth,
|
||||
// Keine quadratischen Karten mehr, stattdessen festgelegte Höhen
|
||||
height: 280,
|
||||
minHeight: 220,
|
||||
maxHeight: 320
|
||||
}
|
||||
]}
|
||||
onPress={() => navigateToConversation(doc.conversation_id)}
|
||||
>
|
||||
<View style={styles.documentHeader}>
|
||||
<Text style={[styles.documentTitle, { color: colors.text }]}>
|
||||
{extractDocumentTitle(doc.content)}
|
||||
</Text>
|
||||
<View style={styles.documentMeta}>
|
||||
<Text style={[styles.conversationTitle, { color: colors.text }]}>
|
||||
{doc.conversation_title}
|
||||
</Text>
|
||||
<View style={styles.metaRight}>
|
||||
<Text style={[styles.documentDate, { color: colors.text }]}>
|
||||
{formatDate(doc.updated_at)}
|
||||
</Text>
|
||||
<Text style={[styles.documentVersion, { color: colors.text }]}>
|
||||
v{doc.version}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.contentContainer}>
|
||||
<ScrollView style={styles.documentContent} nestedScrollEnabled={true}>
|
||||
<Markdown
|
||||
style={{
|
||||
body: {
|
||||
color: colors.text,
|
||||
fontSize: 13,
|
||||
lineHeight: 18
|
||||
},
|
||||
// Normale Anzeige für H1-Überschriften im Inhalt
|
||||
heading1: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
marginTop: 8,
|
||||
marginBottom: 6,
|
||||
lineHeight: 20,
|
||||
paddingBottom: 4,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(0,0,0,0.1)',
|
||||
},
|
||||
heading2: {
|
||||
color: colors.text,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginVertical: 5,
|
||||
lineHeight: 18
|
||||
},
|
||||
paragraph: {
|
||||
color: colors.text,
|
||||
marginBottom: 8,
|
||||
fontSize: 13,
|
||||
lineHeight: 18
|
||||
},
|
||||
blockquote: {
|
||||
backgroundColor: colors.card,
|
||||
borderLeftColor: colors.primary,
|
||||
borderLeftWidth: 2,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
marginVertical: 6
|
||||
},
|
||||
code_block: {
|
||||
backgroundColor: colors.card,
|
||||
padding: 6,
|
||||
borderRadius: 3,
|
||||
fontSize: 12,
|
||||
lineHeight: 16
|
||||
},
|
||||
link: { color: colors.primary }
|
||||
}}
|
||||
>
|
||||
{removeHeadingFromContent(doc.content, extractDocumentTitle(doc.content))}
|
||||
</Markdown>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
// 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 (
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: colors.text }]}>Alle Dokumente</Text>
|
||||
<TouchableOpacity style={styles.refreshButton} onPress={loadDocuments}>
|
||||
<Ionicons name="refresh" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: colors.text }]}>
|
||||
Dokumente werden geladen...
|
||||
</Text>
|
||||
</View>
|
||||
) : documents.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons
|
||||
name="document-text-outline"
|
||||
size={64}
|
||||
color={colors.text}
|
||||
style={styles.emptyIcon}
|
||||
/>
|
||||
<Text style={[styles.emptyText, { color: colors.text }]}>Keine Dokumente gefunden</Text>
|
||||
<Text style={[styles.emptySubtext, { color: colors.text }]}>
|
||||
Erstelle ein neues Dokument in einer Konversation mit aktiviertem Dokumentmodus.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
style={styles.scrollContainer}
|
||||
contentContainerStyle={styles.documentsContainer}
|
||||
>
|
||||
{documents.map((doc) => (
|
||||
<TouchableOpacity
|
||||
key={doc.id}
|
||||
style={[
|
||||
styles.documentCard,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
width: cardWidth,
|
||||
// Keine quadratischen Karten mehr, stattdessen festgelegte Höhen
|
||||
height: 280,
|
||||
minHeight: 220,
|
||||
maxHeight: 320,
|
||||
},
|
||||
]}
|
||||
onPress={() => navigateToConversation(doc.conversation_id)}
|
||||
>
|
||||
<View style={styles.documentHeader}>
|
||||
<Text style={[styles.documentTitle, { color: colors.text }]}>
|
||||
{extractDocumentTitle(doc.content)}
|
||||
</Text>
|
||||
<View style={styles.documentMeta}>
|
||||
<Text style={[styles.conversationTitle, { color: colors.text }]}>
|
||||
{doc.conversation_title}
|
||||
</Text>
|
||||
<View style={styles.metaRight}>
|
||||
<Text style={[styles.documentDate, { color: colors.text }]}>
|
||||
{formatDate(doc.updated_at)}
|
||||
</Text>
|
||||
<Text style={[styles.documentVersion, { color: colors.text }]}>
|
||||
v{doc.version}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.contentContainer}>
|
||||
<ScrollView style={styles.documentContent} nestedScrollEnabled={true}>
|
||||
<Markdown
|
||||
style={{
|
||||
body: {
|
||||
color: colors.text,
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
// Normale Anzeige für H1-Überschriften im Inhalt
|
||||
heading1: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
marginTop: 8,
|
||||
marginBottom: 6,
|
||||
lineHeight: 20,
|
||||
paddingBottom: 4,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(0,0,0,0.1)',
|
||||
},
|
||||
heading2: {
|
||||
color: colors.text,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginVertical: 5,
|
||||
lineHeight: 18,
|
||||
},
|
||||
paragraph: {
|
||||
color: colors.text,
|
||||
marginBottom: 8,
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
blockquote: {
|
||||
backgroundColor: colors.card,
|
||||
borderLeftColor: colors.primary,
|
||||
borderLeftWidth: 2,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
marginVertical: 6,
|
||||
},
|
||||
code_block: {
|
||||
backgroundColor: colors.card,
|
||||
padding: 6,
|
||||
borderRadius: 3,
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
},
|
||||
link: { color: colors.primary },
|
||||
}}
|
||||
>
|
||||
{removeHeadingFromContent(doc.content, extractDocumentTitle(doc.content))}
|
||||
</Markdown>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -10,169 +10,164 @@ 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<Model[]>(availableModels);
|
||||
const [selectedModelId, setSelectedModelId] = useState<string>(availableModels[0].id);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { colors } = useTheme();
|
||||
const router = useRouter();
|
||||
const params = useLocalSearchParams();
|
||||
const initialMessage = (params.initialMessage as string) || '';
|
||||
const [models, setModels] = useState<Model[]>(availableModels);
|
||||
const [selectedModelId, setSelectedModelId] = useState<string>(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);
|
||||
}
|
||||
};
|
||||
// Extrahiere mögliche Space ID aus den Parametern
|
||||
const spaceId = (params.spaceId as string) || null;
|
||||
|
||||
loadModels();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
// Lade Modelle vom Service
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const modelsList = await getModels();
|
||||
setModels(modelsList);
|
||||
|
||||
const handleSelectModel = (id: string) => {
|
||||
setSelectedModelId(id);
|
||||
};
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
};
|
||||
loadModels();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
style={styles.backButton}
|
||||
>
|
||||
<Ionicons name="arrow-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: colors.text }]}>
|
||||
Modell auswählen
|
||||
</Text>
|
||||
</View>
|
||||
const handleSelectModel = (id: string) => {
|
||||
setSelectedModelId(id);
|
||||
};
|
||||
|
||||
<Text style={[styles.subtitle, { color: colors.text + 'CC' }]}>
|
||||
Wähle das KI-Modell, mit dem du chatten möchtest
|
||||
</Text>
|
||||
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
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={[styles.loadingText, { color: colors.text + '80' }]}>
|
||||
Modelle werden geladen...
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={models}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<ModelCard
|
||||
id={item.id}
|
||||
name={item.name}
|
||||
description={item.description}
|
||||
isSelected={item.id === selectedModelId}
|
||||
onSelect={handleSelectModel}
|
||||
model={item}
|
||||
/>
|
||||
)}
|
||||
contentContainerStyle={styles.listContent}
|
||||
/>
|
||||
)}
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
|
||||
<Ionicons name="arrow-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: colors.text }]}>Modell auswählen</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.startButton, { backgroundColor: colors.primary }]}
|
||||
onPress={handleStart}
|
||||
>
|
||||
<Text style={styles.startButtonText}>Konversation starten</Text>
|
||||
<Ionicons name="arrow-forward" size={18} color="white" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
<Text style={[styles.subtitle, { color: colors.text + 'CC' }]}>
|
||||
Wähle das KI-Modell, mit dem du chatten möchtest
|
||||
</Text>
|
||||
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={[styles.loadingText, { color: colors.text + '80' }]}>
|
||||
Modelle werden geladen...
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={models}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<ModelCard
|
||||
id={item.id}
|
||||
name={item.name}
|
||||
description={item.description}
|
||||
isSelected={item.id === selectedModelId}
|
||||
onSelect={handleSelectModel}
|
||||
model={item}
|
||||
/>
|
||||
)}
|
||||
contentContainerStyle={styles.listContent}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={styles.footer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.startButton, { backgroundColor: colors.primary }]}
|
||||
onPress={handleStart}
|
||||
>
|
||||
<Text style={styles.startButtonText}>Konversation starten</Text>
|
||||
<Ionicons name="arrow-forward" size={18} color="white" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,16 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { View, Text, StyleSheet, FlatList, TouchableOpacity, SafeAreaView, Alert, ActivityIndicator, Pressable, Platform } from 'react-native';
|
||||
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';
|
||||
|
|
@ -7,497 +18,489 @@ 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<Space[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [expandedSpaceId, setExpandedSpaceId] = useState<string | null>(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 (
|
||||
<View style={[
|
||||
styles.spaceItemWrapper,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2
|
||||
}
|
||||
]}>
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.spaceItem,
|
||||
hovered && { backgroundColor: colors.cardHover },
|
||||
pressed && { opacity: 0.9 }
|
||||
]}
|
||||
onPress={() => handleSpacePress(item.id)}
|
||||
onLongPress={() => toggleOptionsMenu(item.id)}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<>
|
||||
<View style={styles.spaceContent}>
|
||||
<View style={styles.spaceHeader}>
|
||||
<View style={styles.titleRow}>
|
||||
<Ionicons
|
||||
name="people-outline"
|
||||
size={18}
|
||||
color={colors.primary}
|
||||
style={styles.titleIcon}
|
||||
/>
|
||||
<Text style={[styles.title, { color: colors.text }]} numberOfLines={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
{isOwner && (
|
||||
<View style={[styles.ownerBadge, { backgroundColor: colors.primary + '20' }]}>
|
||||
<Text style={[styles.ownerBadgeText, { color: colors.primary }]}>
|
||||
Besitzer
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{item.description && (
|
||||
<Text
|
||||
style={[styles.description, { color: colors.text + 'CC' }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{item.description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<View style={styles.statsContainer}>
|
||||
<Text style={[styles.timestamp, { color: colors.text + '80' }]}>
|
||||
Erstellt: {new Date(item.created_at).toLocaleDateString()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.optionsButton,
|
||||
hovered && { backgroundColor: colors.menuItemHover },
|
||||
pressed && { opacity: 0.7 }
|
||||
]}
|
||||
onPress={() => toggleOptionsMenu(item.id)}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<Ionicons
|
||||
name="ellipsis-vertical"
|
||||
size={20}
|
||||
color={colors.text + '80'}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
{showOptions && (
|
||||
<View style={[styles.optionsContainer, {
|
||||
backgroundColor: colors.card,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border
|
||||
}]}>
|
||||
{isOwner && (
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.optionButton,
|
||||
hovered && { backgroundColor: colors.menuItemHover },
|
||||
pressed && { opacity: 0.8 }
|
||||
]}
|
||||
onPress={() => router.push(`/spaces/${item.id}/settings`)}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<>
|
||||
<Ionicons name="settings-outline" size={18} color={colors.text} />
|
||||
<Text style={[styles.optionText, { color: colors.text }]}>Einstellungen</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.optionButton,
|
||||
hovered && { backgroundColor: colors.menuItemHover },
|
||||
pressed && { opacity: 0.8 }
|
||||
]}
|
||||
onPress={() => router.push(`/spaces/${item.id}/invite`)}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<>
|
||||
<Ionicons name="person-add-outline" size={18} color={colors.text} />
|
||||
<Text style={[styles.optionText, { color: colors.text }]}>Einladen</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
{isOwner ? (
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.optionButton,
|
||||
hovered && { backgroundColor: colors.dangerHover },
|
||||
pressed && { opacity: 0.8 }
|
||||
]}
|
||||
onPress={() => handleDeleteSpace(item.id)}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<>
|
||||
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
|
||||
<Text style={[styles.optionText, { color: "#FF3B30" }]}>Löschen</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
) : (
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.optionButton,
|
||||
hovered && { backgroundColor: colors.dangerHover },
|
||||
pressed && { opacity: 0.8 }
|
||||
]}
|
||||
onPress={() => handleLeaveSpace(item.id)}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<>
|
||||
<Ionicons name="exit-outline" size={18} color="#FF3B30" />
|
||||
<Text style={[styles.optionText, { color: "#FF3B30" }]}>Verlassen</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
const { colors } = useTheme();
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const [spaces, setSpaces] = useState<Space[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [expandedSpaceId, setExpandedSpaceId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: colors.text }]}>Spaces</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.contentContainer}>
|
||||
{/* Create new space button */}
|
||||
<TouchableOpacity
|
||||
style={[styles.createSpaceButton, { backgroundColor: colors.primary }]}
|
||||
onPress={handleCreateSpace}
|
||||
>
|
||||
<Ionicons name="add" size={24} color="white" />
|
||||
<Text style={styles.createSpaceText}>Neuen Space erstellen</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Space list */}
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: colors.text + '80' }]}>
|
||||
Spaces werden geladen...
|
||||
</Text>
|
||||
</View>
|
||||
) : spaces.length > 0 ? (
|
||||
<FlatList
|
||||
data={spaces}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderSpaceItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons
|
||||
name="people-outline"
|
||||
size={64}
|
||||
color={colors.text + '40'}
|
||||
/>
|
||||
<Text style={[styles.emptyText, { color: colors.text + '80' }]}>
|
||||
Keine Spaces gefunden
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtext, { color: colors.text + '60' }]}>
|
||||
Erstelle einen neuen Space oder frage nach einer Einladung
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
// 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 (
|
||||
<View
|
||||
style={[
|
||||
styles.spaceItemWrapper,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.spaceItem,
|
||||
hovered && { backgroundColor: colors.cardHover },
|
||||
pressed && { opacity: 0.9 },
|
||||
]}
|
||||
onPress={() => handleSpacePress(item.id)}
|
||||
onLongPress={() => toggleOptionsMenu(item.id)}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<>
|
||||
<View style={styles.spaceContent}>
|
||||
<View style={styles.spaceHeader}>
|
||||
<View style={styles.titleRow}>
|
||||
<Ionicons
|
||||
name="people-outline"
|
||||
size={18}
|
||||
color={colors.primary}
|
||||
style={styles.titleIcon}
|
||||
/>
|
||||
<Text style={[styles.title, { color: colors.text }]} numberOfLines={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
{isOwner && (
|
||||
<View style={[styles.ownerBadge, { backgroundColor: colors.primary + '20' }]}>
|
||||
<Text style={[styles.ownerBadgeText, { color: colors.primary }]}>
|
||||
Besitzer
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{item.description && (
|
||||
<Text
|
||||
style={[styles.description, { color: colors.text + 'CC' }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{item.description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<View style={styles.statsContainer}>
|
||||
<Text style={[styles.timestamp, { color: colors.text + '80' }]}>
|
||||
Erstellt: {new Date(item.created_at).toLocaleDateString()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.optionsButton,
|
||||
hovered && { backgroundColor: colors.menuItemHover },
|
||||
pressed && { opacity: 0.7 },
|
||||
]}
|
||||
onPress={() => toggleOptionsMenu(item.id)}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<Ionicons name="ellipsis-vertical" size={20} color={colors.text + '80'} />
|
||||
)}
|
||||
</Pressable>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
{showOptions && (
|
||||
<View
|
||||
style={[
|
||||
styles.optionsContainer,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{isOwner && (
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.optionButton,
|
||||
hovered && { backgroundColor: colors.menuItemHover },
|
||||
pressed && { opacity: 0.8 },
|
||||
]}
|
||||
onPress={() => router.push(`/spaces/${item.id}/settings`)}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<>
|
||||
<Ionicons name="settings-outline" size={18} color={colors.text} />
|
||||
<Text style={[styles.optionText, { color: colors.text }]}>Einstellungen</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.optionButton,
|
||||
hovered && { backgroundColor: colors.menuItemHover },
|
||||
pressed && { opacity: 0.8 },
|
||||
]}
|
||||
onPress={() => router.push(`/spaces/${item.id}/invite`)}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<>
|
||||
<Ionicons name="person-add-outline" size={18} color={colors.text} />
|
||||
<Text style={[styles.optionText, { color: colors.text }]}>Einladen</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
{isOwner ? (
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.optionButton,
|
||||
hovered && { backgroundColor: colors.dangerHover },
|
||||
pressed && { opacity: 0.8 },
|
||||
]}
|
||||
onPress={() => handleDeleteSpace(item.id)}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<>
|
||||
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
|
||||
<Text style={[styles.optionText, { color: '#FF3B30' }]}>Löschen</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
) : (
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.optionButton,
|
||||
hovered && { backgroundColor: colors.dangerHover },
|
||||
pressed && { opacity: 0.8 },
|
||||
]}
|
||||
onPress={() => handleLeaveSpace(item.id)}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<>
|
||||
<Ionicons name="exit-outline" size={18} color="#FF3B30" />
|
||||
<Text style={[styles.optionText, { color: '#FF3B30' }]}>Verlassen</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
||||
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: colors.text }]}>Spaces</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.contentContainer}>
|
||||
{/* Create new space button */}
|
||||
<TouchableOpacity
|
||||
style={[styles.createSpaceButton, { backgroundColor: colors.primary }]}
|
||||
onPress={handleCreateSpace}
|
||||
>
|
||||
<Ionicons name="add" size={24} color="white" />
|
||||
<Text style={styles.createSpaceText}>Neuen Space erstellen</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Space list */}
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: colors.text + '80' }]}>
|
||||
Spaces werden geladen...
|
||||
</Text>
|
||||
</View>
|
||||
) : spaces.length > 0 ? (
|
||||
<FlatList
|
||||
data={spaces}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderSpaceItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="people-outline" size={64} color={colors.text + '40'} />
|
||||
<Text style={[styles.emptyText, { color: colors.text + '80' }]}>
|
||||
Keine Spaces gefunden
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtext, { color: colors.text + '60' }]}>
|
||||
Erstelle einen neuen Space oder frage nach einer Einladung
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, TextInput, SafeAreaView, Alert, ActivityIndicator, ScrollView } from 'react-native';
|
||||
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';
|
||||
|
|
@ -7,208 +17,211 @@ 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);
|
||||
}
|
||||
};
|
||||
const { colors } = useTheme();
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: colors.text }]}>Neuen Space erstellen</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.contentContainer} contentContainerStyle={styles.scrollContent}>
|
||||
<View style={styles.formSection}>
|
||||
<Text style={[styles.label, { color: colors.text }]}>Name *</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
color: colors.text
|
||||
}
|
||||
]}
|
||||
placeholder="Name des Spaces"
|
||||
placeholderTextColor={colors.text + '70'}
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
maxLength={50}
|
||||
/>
|
||||
|
||||
<Text style={[styles.label, { color: colors.text, marginTop: 20 }]}>Beschreibung</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.textArea,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
color: colors.text
|
||||
}
|
||||
]}
|
||||
placeholder="Beschreibung des Spaces (optional)"
|
||||
placeholderTextColor={colors.text + '70'}
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
maxLength={500}
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.infoSection}>
|
||||
<View style={styles.infoItem}>
|
||||
<Ionicons name="information-circle-outline" size={20} color={colors.text + '80'} style={styles.infoIcon} />
|
||||
<Text style={[styles.infoText, { color: colors.text + '80' }]}>
|
||||
Spaces sind Bereiche zum Organisieren von Konversationen und können mit anderen Nutzern geteilt werden.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={[styles.footer, { borderTopColor: colors.border }]}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.createButton,
|
||||
{
|
||||
backgroundColor: isValid ? colors.primary : colors.primary + '50',
|
||||
opacity: isCreating ? 0.7 : 1
|
||||
}
|
||||
]}
|
||||
onPress={handleCreateSpace}
|
||||
disabled={!isValid || isCreating}
|
||||
>
|
||||
{isCreating ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<Text style={styles.createButtonText}>Space erstellen</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
// 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 (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
||||
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: colors.text }]}>Neuen Space erstellen</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.contentContainer} contentContainerStyle={styles.scrollContent}>
|
||||
<View style={styles.formSection}>
|
||||
<Text style={[styles.label, { color: colors.text }]}>Name *</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
color: colors.text,
|
||||
},
|
||||
]}
|
||||
placeholder="Name des Spaces"
|
||||
placeholderTextColor={colors.text + '70'}
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
maxLength={50}
|
||||
/>
|
||||
|
||||
<Text style={[styles.label, { color: colors.text, marginTop: 20 }]}>Beschreibung</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.textArea,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.border,
|
||||
color: colors.text,
|
||||
},
|
||||
]}
|
||||
placeholder="Beschreibung des Spaces (optional)"
|
||||
placeholderTextColor={colors.text + '70'}
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
maxLength={500}
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.infoSection}>
|
||||
<View style={styles.infoItem}>
|
||||
<Ionicons
|
||||
name="information-circle-outline"
|
||||
size={20}
|
||||
color={colors.text + '80'}
|
||||
style={styles.infoIcon}
|
||||
/>
|
||||
<Text style={[styles.infoText, { color: colors.text + '80' }]}>
|
||||
Spaces sind Bereiche zum Organisieren von Konversationen und können mit anderen
|
||||
Nutzern geteilt werden.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={[styles.footer, { borderTopColor: colors.border }]}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.createButton,
|
||||
{
|
||||
backgroundColor: isValid ? colors.primary : colors.primary + '50',
|
||||
opacity: isCreating ? 0.7 : 1,
|
||||
},
|
||||
]}
|
||||
onPress={handleCreateSpace}
|
||||
disabled={!isValid || isCreating}
|
||||
>
|
||||
{isCreating ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<Text style={styles.createButtonText}>Space erstellen</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
SafeAreaView,
|
||||
Alert,
|
||||
Modal,
|
||||
ActivityIndicator
|
||||
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';
|
||||
|
|
@ -19,417 +19,399 @@ 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
|
||||
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<Template[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isFormModalVisible, setIsFormModalVisible] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(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<Template>) => {
|
||||
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 (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.mainLayout}>
|
||||
{/* Drawer / Seitenmenü */}
|
||||
{isDrawerOpen && (
|
||||
<View style={styles.drawerContainer}>
|
||||
<CustomDrawer
|
||||
isVisible={isDrawerOpen}
|
||||
onClose={() => setIsDrawerOpen(false)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Hauptinhalt */}
|
||||
<View style={styles.mainContainer}>
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={styles.menuButton}
|
||||
onPress={() => setIsDrawerOpen(!isDrawerOpen)}
|
||||
>
|
||||
<Ionicons
|
||||
name="menu-outline"
|
||||
size={28}
|
||||
color={colors.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.title, { color: colors.text }]}>Vorlagen</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, { backgroundColor: colors.primary }]}
|
||||
onPress={handleCreateTemplate}
|
||||
>
|
||||
<Ionicons name="add" size={20} color="white" />
|
||||
<Text style={styles.addButtonText}>Neue Vorlage</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Beschreibung */}
|
||||
<View style={styles.descriptionContainer}>
|
||||
<Text style={[styles.description, { color: colors.text + 'CC' }]}>
|
||||
Erstelle Vorlagen mit benutzerdefinierten System-Prompts für verschiedene KI-Verhaltensweisen.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Vorlagenliste */}
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: colors.text + '80' }]}>
|
||||
Vorlagen werden geladen...
|
||||
</Text>
|
||||
</View>
|
||||
) : templates.length > 0 ? (
|
||||
<FlatList
|
||||
data={templates}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TemplateCard
|
||||
id={item.id}
|
||||
name={item.name}
|
||||
description={item.description}
|
||||
systemPrompt={item.system_prompt}
|
||||
color={item.color}
|
||||
isDefault={item.is_default}
|
||||
onPress={handleUseTemplate}
|
||||
onEdit={handleEditTemplate}
|
||||
onDelete={handleDeleteTemplate}
|
||||
onSetDefault={handleSetDefaultTemplate}
|
||||
/>
|
||||
)}
|
||||
contentContainerStyle={styles.listContent}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons
|
||||
name="document-text-outline"
|
||||
size={64}
|
||||
color={colors.text + '40'}
|
||||
/>
|
||||
<Text style={[styles.emptyText, { color: colors.text + '80' }]}>
|
||||
Keine Vorlagen vorhanden
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtext, { color: colors.text + '60' }]}>
|
||||
Erstelle deine erste Vorlage, um loszulegen
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Modal für das Erstellen/Bearbeiten von Vorlagen */}
|
||||
<Modal
|
||||
visible={isFormModalVisible}
|
||||
animationType="slide"
|
||||
transparent={false}
|
||||
onRequestClose={() => setIsFormModalVisible(false)}
|
||||
>
|
||||
<SafeAreaView style={styles.modalContainer}>
|
||||
<TemplateForm
|
||||
initialData={selectedTemplate || undefined}
|
||||
onSubmit={handleSubmitTemplate}
|
||||
onCancel={() => setIsFormModalVisible(false)}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
const { colors } = useTheme();
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { isDarkMode } = useAppTheme();
|
||||
|
||||
const [templates, setTemplates] = useState<Template[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isFormModalVisible, setIsFormModalVisible] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(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<Template>) => {
|
||||
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 (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.mainLayout}>
|
||||
{/* Drawer / Seitenmenü */}
|
||||
{isDrawerOpen && (
|
||||
<View style={styles.drawerContainer}>
|
||||
<CustomDrawer isVisible={isDrawerOpen} onClose={() => setIsDrawerOpen(false)} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Hauptinhalt */}
|
||||
<View style={styles.mainContainer}>
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={styles.menuButton}
|
||||
onPress={() => setIsDrawerOpen(!isDrawerOpen)}
|
||||
>
|
||||
<Ionicons name="menu-outline" size={28} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.title, { color: colors.text }]}>Vorlagen</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, { backgroundColor: colors.primary }]}
|
||||
onPress={handleCreateTemplate}
|
||||
>
|
||||
<Ionicons name="add" size={20} color="white" />
|
||||
<Text style={styles.addButtonText}>Neue Vorlage</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Beschreibung */}
|
||||
<View style={styles.descriptionContainer}>
|
||||
<Text style={[styles.description, { color: colors.text + 'CC' }]}>
|
||||
Erstelle Vorlagen mit benutzerdefinierten System-Prompts für verschiedene
|
||||
KI-Verhaltensweisen.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Vorlagenliste */}
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: colors.text + '80' }]}>
|
||||
Vorlagen werden geladen...
|
||||
</Text>
|
||||
</View>
|
||||
) : templates.length > 0 ? (
|
||||
<FlatList
|
||||
data={templates}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TemplateCard
|
||||
id={item.id}
|
||||
name={item.name}
|
||||
description={item.description}
|
||||
systemPrompt={item.system_prompt}
|
||||
color={item.color}
|
||||
isDefault={item.is_default}
|
||||
onPress={handleUseTemplate}
|
||||
onEdit={handleEditTemplate}
|
||||
onDelete={handleDeleteTemplate}
|
||||
onSetDefault={handleSetDefaultTemplate}
|
||||
/>
|
||||
)}
|
||||
contentContainerStyle={styles.listContent}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="document-text-outline" size={64} color={colors.text + '40'} />
|
||||
<Text style={[styles.emptyText, { color: colors.text + '80' }]}>
|
||||
Keine Vorlagen vorhanden
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtext, { color: colors.text + '60' }]}>
|
||||
Erstelle deine erste Vorlage, um loszulegen
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Modal für das Erstellen/Bearbeiten von Vorlagen */}
|
||||
<Modal
|
||||
visible={isFormModalVisible}
|
||||
animationType="slide"
|
||||
transparent={false}
|
||||
onRequestClose={() => setIsFormModalVisible(false)}
|
||||
>
|
||||
<SafeAreaView style={styles.modalContainer}>
|
||||
<TemplateForm
|
||||
initialData={selectedTemplate || undefined}
|
||||
onSubmit={handleSubmitTemplate}
|
||||
onCancel={() => setIsFormModalVisible(false)}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
const plugins = [];
|
||||
api.cache(true);
|
||||
const plugins = [];
|
||||
|
||||
plugins.push('react-native-reanimated/plugin');
|
||||
plugins.push('react-native-reanimated/plugin');
|
||||
|
||||
return {
|
||||
presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
|
||||
return {
|
||||
presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
|
||||
|
||||
plugins,
|
||||
};
|
||||
plugins,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,40 +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"
|
||||
}
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,21 +2,22 @@ import { forwardRef } from 'react';
|
|||
import { Text, TouchableOpacity, TouchableOpacityProps, View } from 'react-native';
|
||||
|
||||
type ButtonProps = {
|
||||
title: string;
|
||||
title: string;
|
||||
} & TouchableOpacityProps;
|
||||
|
||||
export const Button = forwardRef<View, ButtonProps>(({ title, ...touchableProps }, ref) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
ref={ref}
|
||||
{...touchableProps}
|
||||
className={`${styles.button} ${touchableProps.className}`}>
|
||||
<Text className={styles.buttonText}>{title}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
ref={ref}
|
||||
{...touchableProps}
|
||||
className={`${styles.button} ${touchableProps.className}`}
|
||||
>
|
||||
<Text className={styles.buttonText}>{title}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = {
|
||||
button: 'items-center bg-indigo-500 rounded-[28px] shadow-md p-4',
|
||||
buttonText: 'text-white text-lg font-semibold text-center',
|
||||
button: 'items-center bg-indigo-500 rounded-[28px] shadow-md p-4',
|
||||
buttonText: 'text-white text-lg font-semibold text-center',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,89 +5,88 @@ import { Ionicons } from '@expo/vector-icons';
|
|||
import { useRouter } from 'expo-router';
|
||||
|
||||
type ChatHeaderProps = {
|
||||
title?: string;
|
||||
modelName: string;
|
||||
conversationMode: string;
|
||||
onBackPress?: () => void;
|
||||
title?: string;
|
||||
modelName: string;
|
||||
conversationMode: string;
|
||||
onBackPress?: () => void;
|
||||
};
|
||||
|
||||
export default function ChatHeader({
|
||||
title,
|
||||
modelName,
|
||||
conversationMode,
|
||||
onBackPress
|
||||
export default function ChatHeader({
|
||||
title,
|
||||
modelName,
|
||||
conversationMode,
|
||||
onBackPress,
|
||||
}: ChatHeaderProps) {
|
||||
const { colors } = useTheme();
|
||||
const router = useRouter();
|
||||
const { colors } = useTheme();
|
||||
const router = useRouter();
|
||||
|
||||
const handleBackPress = () => {
|
||||
if (onBackPress) {
|
||||
onBackPress();
|
||||
} else {
|
||||
router.back();
|
||||
}
|
||||
};
|
||||
const handleBackPress = () => {
|
||||
if (onBackPress) {
|
||||
onBackPress();
|
||||
} else {
|
||||
router.back();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={[styles.title, { color: colors.text }]}>
|
||||
{title || 'Neuer Chat'}
|
||||
</Text>
|
||||
<View style={styles.subtitleContainer}>
|
||||
<Text style={[styles.modelName, { color: colors.text + '80' }]}>
|
||||
{modelName}
|
||||
</Text>
|
||||
<Text style={[styles.modeText, { color: colors.text + '80' }]}>
|
||||
{conversationMode === 'frei' ? 'Freier Modus' :
|
||||
conversationMode === 'geführt' ? 'Geführter Modus' : 'Vorlagen-Modus'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.menuButton}>
|
||||
<Ionicons name="ellipsis-vertical" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={[styles.title, { color: colors.text }]}>{title || 'Neuer Chat'}</Text>
|
||||
<View style={styles.subtitleContainer}>
|
||||
<Text style={[styles.modelName, { color: colors.text + '80' }]}>{modelName}</Text>
|
||||
<Text style={[styles.modeText, { color: colors.text + '80' }]}>
|
||||
{conversationMode === 'frei'
|
||||
? 'Freier Modus'
|
||||
: conversationMode === 'geführt'
|
||||
? 'Geführter Modus'
|
||||
: 'Vorlagen-Modus'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.menuButton}>
|
||||
<Ionicons name="ellipsis-vertical" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,118 +5,101 @@ 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;
|
||||
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,
|
||||
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,
|
||||
});
|
||||
const { text, setText, handleSend, canSend, isDarkMode } = useChatInput({
|
||||
onSend,
|
||||
isLoading,
|
||||
placeholder,
|
||||
});
|
||||
|
||||
return (
|
||||
<View className="w-full px-4">
|
||||
<View className={`rounded-lg p-4 ${isDarkMode ? 'bg-[#2C2C2E]' : 'bg-white'}`}>
|
||||
{showModelSelection && (
|
||||
<View className="flex-row justify-between items-center mb-3">
|
||||
<Text className={`text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
|
||||
Modell:
|
||||
</Text>
|
||||
<ModelDropdown
|
||||
selectedModelId={selectedModelId}
|
||||
onSelectModel={onSelectModel}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
className={`w-full min-h-[40px] text-base rounded-lg px-4 py-2 ${
|
||||
isDarkMode
|
||||
? 'text-white bg-[#1C1C1E]'
|
||||
: 'text-black bg-gray-100'
|
||||
}`}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={isDarkMode ? '#8E8E93' : '#8E8E93'}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
multiline
|
||||
maxLength={1000}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
|
||||
<View className="flex-row justify-between items-center mt-4">
|
||||
{(showAttachments || showSearch) && (
|
||||
<View className="flex-row space-x-4">
|
||||
{showAttachments && (
|
||||
<TouchableOpacity className="flex-row items-center">
|
||||
<Ionicons name="attach" size={20} color={isDarkMode ? '#FFFFFF' : '#000000'} />
|
||||
<Text className={`ml-1 ${isDarkMode ? 'text-white' : 'text-black'}`}>Attach</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{showSearch && (
|
||||
<TouchableOpacity className="flex-row items-center">
|
||||
<Ionicons name="search" size={20} color={isDarkMode ? '#FFFFFF' : '#000000'} />
|
||||
<Text className={`ml-1 ${isDarkMode ? 'text-white' : 'text-black'}`}>Search</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center px-3 py-2 rounded-full ${
|
||||
canSend ? 'bg-[#0A84FF]' : 'bg-[#0A84FF]/20'
|
||||
}`}
|
||||
onPress={handleSend}
|
||||
disabled={!canSend}
|
||||
>
|
||||
{isLoading ? (
|
||||
<View className="flex-row items-center">
|
||||
<View className="h-4 w-4 mr-1">
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
</View>
|
||||
<Text className="text-white">Wird gesendet...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="send"
|
||||
size={18}
|
||||
color={canSend ? '#FFFFFF' : '#0A84FF'}
|
||||
/>
|
||||
<Text
|
||||
className={`ml-1 ${canSend ? 'text-white' : 'text-[#0A84FF]'}`}
|
||||
>
|
||||
Senden
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View className="w-full px-4">
|
||||
<View className={`rounded-lg p-4 ${isDarkMode ? 'bg-[#2C2C2E]' : 'bg-white'}`}>
|
||||
{showModelSelection && (
|
||||
<View className="mb-3 flex-row items-center justify-between">
|
||||
<Text
|
||||
className={`text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
|
||||
>
|
||||
Modell:
|
||||
</Text>
|
||||
<ModelDropdown selectedModelId={selectedModelId} onSelectModel={onSelectModel} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
className={`min-h-[40px] w-full rounded-lg px-4 py-2 text-base ${
|
||||
isDarkMode ? 'bg-[#1C1C1E] text-white' : 'bg-gray-100 text-black'
|
||||
}`}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={isDarkMode ? '#8E8E93' : '#8E8E93'}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
multiline
|
||||
maxLength={1000}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
|
||||
<View className="mt-4 flex-row items-center justify-between">
|
||||
{(showAttachments || showSearch) && (
|
||||
<View className="flex-row space-x-4">
|
||||
{showAttachments && (
|
||||
<TouchableOpacity className="flex-row items-center">
|
||||
<Ionicons name="attach" size={20} color={isDarkMode ? '#FFFFFF' : '#000000'} />
|
||||
<Text className={`ml-1 ${isDarkMode ? 'text-white' : 'text-black'}`}>Attach</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{showSearch && (
|
||||
<TouchableOpacity className="flex-row items-center">
|
||||
<Ionicons name="search" size={20} color={isDarkMode ? '#FFFFFF' : '#000000'} />
|
||||
<Text className={`ml-1 ${isDarkMode ? 'text-white' : 'text-black'}`}>Search</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center rounded-full px-3 py-2 ${
|
||||
canSend ? 'bg-[#0A84FF]' : 'bg-[#0A84FF]/20'
|
||||
}`}
|
||||
onPress={handleSend}
|
||||
disabled={!canSend}
|
||||
>
|
||||
{isLoading ? (
|
||||
<View className="flex-row items-center">
|
||||
<View className="mr-1 h-4 w-4">
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
</View>
|
||||
<Text className="text-white">Wird gesendet...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="send" size={18} color={canSend ? '#FFFFFF' : '#0A84FF'} />
|
||||
<Text className={`ml-1 ${canSend ? 'text-white' : 'text-[#0A84FF]'}`}>Senden</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import React, { useState, forwardRef, useImperativeHandle, useRef, useEffect } from 'react';
|
||||
import { View, TextInput, TouchableOpacity, Text, ScrollView, StyleSheet, ActivityIndicator } from 'react-native';
|
||||
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';
|
||||
|
|
@ -10,328 +18,344 @@ import { useAuth } from '../context/AuthProvider';
|
|||
import { Template, getTemplates } from '../services/template';
|
||||
|
||||
type ConversationStarterProps = {
|
||||
onSend?: (message: string) => void;
|
||||
placeholder?: string;
|
||||
onSend?: (message: string) => void;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
// Definiere die Ref-Methoden, die von außen aufgerufen werden können
|
||||
export interface ConversationStarterRef {
|
||||
focus: () => void;
|
||||
focus: () => void;
|
||||
}
|
||||
|
||||
const ConversationStarter = forwardRef<ConversationStarterRef, ConversationStarterProps>(({ 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<Template[]>([]);
|
||||
const [isLoadingTemplates, setIsLoadingTemplates] = useState(true);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
|
||||
const { colors } = useTheme();
|
||||
const { isDarkMode } = useAppTheme();
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const inputRef = useRef<TextInput>(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 ConversationStarter = forwardRef<ConversationStarterRef, ConversationStarterProps>(
|
||||
({ 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<Template[]>([]);
|
||||
const [isLoadingTemplates, setIsLoadingTemplates] = useState(true);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
|
||||
const { colors } = useTheme();
|
||||
const { isDarkMode } = useAppTheme();
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
|
||||
const handleSend = async () => {
|
||||
if (text.trim()) {
|
||||
console.log("handleSend wird aufgerufen mit Text:", text.trim());
|
||||
// Expose methods via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
// 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;
|
||||
}
|
||||
// Laden der Vorlagen beim ersten Rendern
|
||||
useEffect(() => {
|
||||
const loadTemplates = async () => {
|
||||
if (!user) 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}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
setIsLoadingTemplates(true);
|
||||
try {
|
||||
const userTemplates = await getTemplates(user.id);
|
||||
setTemplates(userTemplates);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Vorlagen:', error);
|
||||
} finally {
|
||||
setIsLoadingTemplates(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="w-full px-4 max-w-3xl self-center">
|
||||
<View className={`rounded-lg p-4 ${isDarkMode ? 'bg-[#2C2C2E]' : 'bg-white'}`}>
|
||||
<View className="flex-row justify-between items-center mb-3">
|
||||
<Text className={`text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>Modell:</Text>
|
||||
<ModelDropdown
|
||||
selectedModelId={selectedModelId}
|
||||
onSelectModel={setSelectedModelId}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
className={`w-full min-h-[40px] text-base ${isDarkMode ? 'text-white' : 'text-black'}`}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={isDarkMode ? '#8E8E93' : '#8E8E93'}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
multiline
|
||||
maxLength={1000}
|
||||
/>
|
||||
|
||||
<View className="flex-row justify-between items-center mt-4">
|
||||
<View className="flex-row space-x-4">
|
||||
<TouchableOpacity className="flex-row items-center">
|
||||
<Ionicons name="attach" size={20} color={isDarkMode ? '#FFFFFF' : '#000000'} />
|
||||
<Text className={`ml-1 ${isDarkMode ? 'text-white' : 'text-black'}`}>Attach</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity className="flex-row items-center">
|
||||
<Ionicons name="search" size={20} color={isDarkMode ? '#FFFFFF' : '#000000'} />
|
||||
<Text className={`ml-1 ${isDarkMode ? 'text-white' : 'text-black'}`}>Search</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center px-3 py-2 rounded-full ${text.trim() ? 'bg-[#0A84FF]' : 'bg-[#0A84FF]/20'}`}
|
||||
onPress={() => {
|
||||
console.log("Senden-Button gedrückt");
|
||||
handleSend();
|
||||
}}
|
||||
disabled={!text.trim() || isCreatingConversation}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{isCreatingConversation ? (
|
||||
<View className="flex-row items-center">
|
||||
<View className="h-4 w-4 mr-1">
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
</View>
|
||||
<Text className="text-white">Wird erstellt...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="send" size={18} color={text.trim() ? '#FFFFFF' : '#0A84FF'} />
|
||||
<Text className={`ml-1 ${text.trim() ? 'text-white' : 'text-[#0A84FF]'}`}>Senden</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="mt-4">
|
||||
<View>
|
||||
<Text className={`text-sm font-medium mb-1 ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
|
||||
Vorlagen:
|
||||
</Text>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
className="flex-row"
|
||||
>
|
||||
{isLoadingTemplates ? (
|
||||
<View className={`flex-row items-center justify-center mr-2 px-3 py-1 rounded-full border ${
|
||||
isDarkMode ? 'bg-[#2C2C2E] border-[#38383A]' : 'bg-white border-[#E5E5EA]'
|
||||
}`}>
|
||||
<ActivityIndicator size="small" color={isDarkMode ? '#FFFFFF' : '#0A84FF'} style={{marginRight: 6}} />
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
Laden...
|
||||
</Text>
|
||||
</View>
|
||||
) : templates.length > 0 ? (
|
||||
templates.map((template) => (
|
||||
<TouchableOpacity
|
||||
key={template.id}
|
||||
className={`flex-row items-center mr-2 px-3 py-1 rounded-full border ${
|
||||
selectedTemplate?.id === template.id
|
||||
? isDarkMode
|
||||
? 'bg-[#0A84FF]80 border-[#0A84FF]'
|
||||
: 'bg-[#0A84FF]40 border-[#0A84FF]'
|
||||
: isDarkMode
|
||||
? 'bg-[#2C2C2E] border-[#38383A]'
|
||||
: 'bg-white border-[#E5E5EA]'
|
||||
}`}
|
||||
onPress={() => handleTemplateSelect(template)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: template.color || '#0A84FF',
|
||||
marginRight: 6
|
||||
}}
|
||||
/>
|
||||
<Text className={`text-sm ${
|
||||
selectedTemplate?.id === template.id
|
||||
? isDarkMode ? 'text-white font-medium' : 'text-[#0A84FF] font-medium'
|
||||
: isDarkMode ? 'text-white' : 'text-black'
|
||||
}`}>
|
||||
{template.name}
|
||||
</Text>
|
||||
{selectedTemplate?.id === template.id && (
|
||||
<Ionicons
|
||||
name="checkmark-circle"
|
||||
size={14}
|
||||
color={isDarkMode ? '#FFFFFF' : '#0A84FF'}
|
||||
style={{marginLeft: 4}}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center mr-2 px-3 py-1 rounded-full border ${
|
||||
isDarkMode ? 'bg-[#2C2C2E] border-[#38383A]' : 'bg-white border-[#E5E5EA]'
|
||||
}`}
|
||||
onPress={() => router.push('/templates')}
|
||||
>
|
||||
<Ionicons
|
||||
name="add-circle-outline"
|
||||
size={16}
|
||||
color={isDarkMode ? '#FFFFFF' : '#000000'}
|
||||
style={styles.chipIcon}
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
Vorlage erstellen
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center px-3 py-1 rounded-full border ${
|
||||
isDarkMode ? 'bg-[#2C2C2E] border-[#38383A]' : 'bg-white border-[#E5E5EA]'
|
||||
}`}
|
||||
onPress={() => router.push('/templates')}
|
||||
>
|
||||
<Ionicons
|
||||
name="settings-outline"
|
||||
size={16}
|
||||
color={isDarkMode ? '#FFFFFF' : '#000000'}
|
||||
style={styles.chipIcon}
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
Verwalten
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
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 (
|
||||
<View className="w-full max-w-3xl self-center px-4">
|
||||
<View className={`rounded-lg p-4 ${isDarkMode ? 'bg-[#2C2C2E]' : 'bg-white'}`}>
|
||||
<View className="mb-3 flex-row items-center justify-between">
|
||||
<Text
|
||||
className={`text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
|
||||
>
|
||||
Modell:
|
||||
</Text>
|
||||
<ModelDropdown selectedModelId={selectedModelId} onSelectModel={setSelectedModelId} />
|
||||
</View>
|
||||
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
className={`min-h-[40px] w-full text-base ${isDarkMode ? 'text-white' : 'text-black'}`}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={isDarkMode ? '#8E8E93' : '#8E8E93'}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
multiline
|
||||
maxLength={1000}
|
||||
/>
|
||||
|
||||
<View className="mt-4 flex-row items-center justify-between">
|
||||
<View className="flex-row space-x-4">
|
||||
<TouchableOpacity className="flex-row items-center">
|
||||
<Ionicons name="attach" size={20} color={isDarkMode ? '#FFFFFF' : '#000000'} />
|
||||
<Text className={`ml-1 ${isDarkMode ? 'text-white' : 'text-black'}`}>Attach</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity className="flex-row items-center">
|
||||
<Ionicons name="search" size={20} color={isDarkMode ? '#FFFFFF' : '#000000'} />
|
||||
<Text className={`ml-1 ${isDarkMode ? 'text-white' : 'text-black'}`}>Search</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center rounded-full px-3 py-2 ${text.trim() ? 'bg-[#0A84FF]' : 'bg-[#0A84FF]/20'}`}
|
||||
onPress={() => {
|
||||
console.log('Senden-Button gedrückt');
|
||||
handleSend();
|
||||
}}
|
||||
disabled={!text.trim() || isCreatingConversation}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{isCreatingConversation ? (
|
||||
<View className="flex-row items-center">
|
||||
<View className="mr-1 h-4 w-4">
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
</View>
|
||||
<Text className="text-white">Wird erstellt...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="send" size={18} color={text.trim() ? '#FFFFFF' : '#0A84FF'} />
|
||||
<Text className={`ml-1 ${text.trim() ? 'text-white' : 'text-[#0A84FF]'}`}>
|
||||
Senden
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="mt-4">
|
||||
<View>
|
||||
<Text
|
||||
className={`mb-1 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
|
||||
>
|
||||
Vorlagen:
|
||||
</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} className="flex-row">
|
||||
{isLoadingTemplates ? (
|
||||
<View
|
||||
className={`mr-2 flex-row items-center justify-center rounded-full border px-3 py-1 ${
|
||||
isDarkMode ? 'border-[#38383A] bg-[#2C2C2E]' : 'border-[#E5E5EA] bg-white'
|
||||
}`}
|
||||
>
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={isDarkMode ? '#FFFFFF' : '#0A84FF'}
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
Laden...
|
||||
</Text>
|
||||
</View>
|
||||
) : templates.length > 0 ? (
|
||||
templates.map((template) => (
|
||||
<TouchableOpacity
|
||||
key={template.id}
|
||||
className={`mr-2 flex-row items-center rounded-full border px-3 py-1 ${
|
||||
selectedTemplate?.id === template.id
|
||||
? isDarkMode
|
||||
? 'bg-[#0A84FF]80 border-[#0A84FF]'
|
||||
: 'bg-[#0A84FF]40 border-[#0A84FF]'
|
||||
: isDarkMode
|
||||
? 'border-[#38383A] bg-[#2C2C2E]'
|
||||
: 'border-[#E5E5EA] bg-white'
|
||||
}`}
|
||||
onPress={() => handleTemplateSelect(template)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: template.color || '#0A84FF',
|
||||
marginRight: 6,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
className={`text-sm ${
|
||||
selectedTemplate?.id === template.id
|
||||
? isDarkMode
|
||||
? 'font-medium text-white'
|
||||
: 'font-medium text-[#0A84FF]'
|
||||
: isDarkMode
|
||||
? 'text-white'
|
||||
: 'text-black'
|
||||
}`}
|
||||
>
|
||||
{template.name}
|
||||
</Text>
|
||||
{selectedTemplate?.id === template.id && (
|
||||
<Ionicons
|
||||
name="checkmark-circle"
|
||||
size={14}
|
||||
color={isDarkMode ? '#FFFFFF' : '#0A84FF'}
|
||||
style={{ marginLeft: 4 }}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
className={`mr-2 flex-row items-center rounded-full border px-3 py-1 ${
|
||||
isDarkMode ? 'border-[#38383A] bg-[#2C2C2E]' : 'border-[#E5E5EA] bg-white'
|
||||
}`}
|
||||
onPress={() => router.push('/templates')}
|
||||
>
|
||||
<Ionicons
|
||||
name="add-circle-outline"
|
||||
size={16}
|
||||
color={isDarkMode ? '#FFFFFF' : '#000000'}
|
||||
style={styles.chipIcon}
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
Vorlage erstellen
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center rounded-full border px-3 py-1 ${
|
||||
isDarkMode ? 'border-[#38383A] bg-[#2C2C2E]' : 'border-[#E5E5EA] bg-white'
|
||||
}`}
|
||||
onPress={() => router.push('/templates')}
|
||||
>
|
||||
<Ionicons
|
||||
name="settings-outline"
|
||||
size={16}
|
||||
color={isDarkMode ? '#FFFFFF' : '#000000'}
|
||||
style={styles.chipIcon}
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
Verwalten
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Styles für Elemente, die nicht mit NativeWind gestylt werden können
|
||||
const styles = StyleSheet.create({
|
||||
chipIcon: {
|
||||
marginRight: 6,
|
||||
},
|
||||
chipIcon: {
|
||||
marginRight: 6,
|
||||
},
|
||||
});
|
||||
|
||||
export default ConversationStarter;
|
||||
export default ConversationStarter;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { SafeAreaView } from 'react-native';
|
||||
|
||||
export const Container = ({ children }: { children: React.ReactNode }) => {
|
||||
return <SafeAreaView className={styles.container}>{children}</SafeAreaView>;
|
||||
return <SafeAreaView className={styles.container}>{children}</SafeAreaView>;
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: 'flex flex-1 m-6',
|
||||
container: 'flex flex-1 m-6',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import React, { useState, forwardRef, useImperativeHandle, useRef, useEffect } from 'react';
|
||||
import { View, TextInput, TouchableOpacity, Text, ScrollView, StyleSheet, ActivityIndicator } from 'react-native';
|
||||
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';
|
||||
|
|
@ -11,431 +19,458 @@ import { Template, getTemplates } from '../services/template';
|
|||
import { Space, getUserSpaces } from '../services/space';
|
||||
|
||||
type ConversationStarterProps = {
|
||||
onSend?: (message: string) => void;
|
||||
placeholder?: string;
|
||||
spaceId?: string | null;
|
||||
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;
|
||||
focus: () => void;
|
||||
}
|
||||
|
||||
const ConversationStarter = forwardRef<ConversationStarterRef, ConversationStarterProps>(({ 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<Template[]>([]);
|
||||
const [isLoadingTemplates, setIsLoadingTemplates] = useState(true);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
|
||||
const [documentMode, setDocumentMode] = useState(false);
|
||||
const [currentSpace, setCurrentSpace] = useState<Space | null>(null);
|
||||
const { colors } = useTheme();
|
||||
const { isDarkMode } = useAppTheme();
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const inputRef = useRef<TextInput>(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]);
|
||||
const ConversationStarter = forwardRef<ConversationStarterRef, ConversationStarterProps>(
|
||||
({ 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<Template[]>([]);
|
||||
const [isLoadingTemplates, setIsLoadingTemplates] = useState(true);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
|
||||
const [documentMode, setDocumentMode] = useState(false);
|
||||
const [currentSpace, setCurrentSpace] = useState<Space | null>(null);
|
||||
const { colors } = useTheme();
|
||||
const { isDarkMode } = useAppTheme();
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
|
||||
// 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();
|
||||
}
|
||||
};
|
||||
// Expose methods via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const handleSend = async () => {
|
||||
if (text.trim()) {
|
||||
console.log("handleSend wird aufgerufen mit Text:", text.trim());
|
||||
// Laden der Vorlagen und des aktuellen Space beim ersten Rendern
|
||||
useEffect(() => {
|
||||
const loadTemplates = async () => {
|
||||
if (!user) return;
|
||||
|
||||
// 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;
|
||||
}
|
||||
setIsLoadingTemplates(true);
|
||||
try {
|
||||
const userTemplates = await getTemplates(user.id);
|
||||
setTemplates(userTemplates);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Vorlagen:', error);
|
||||
} finally {
|
||||
setIsLoadingTemplates(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
||||
};
|
||||
loadTemplates();
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<View className="w-full px-4 max-w-3xl self-center">
|
||||
{/* Container für den Titel mit fester Höhe - verhindert Layout-Verschiebung */}
|
||||
<View className="h-7 flex-row items-center">
|
||||
{selectedTemplate && (
|
||||
<Text
|
||||
className={`text-base font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{selectedTemplate.name}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{currentSpace && (
|
||||
<View className="flex-row items-center ml-auto">
|
||||
<Ionicons
|
||||
name="folder-open"
|
||||
size={16}
|
||||
color={colors.primary}
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
<Text
|
||||
className={`text-sm font-medium`}
|
||||
style={{ color: colors.primary }}
|
||||
numberOfLines={1}
|
||||
>
|
||||
Space: {currentSpace.name}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View className={`rounded-lg p-4 ${isDarkMode ? 'bg-[#2C2C2E]' : 'bg-white'}`}>
|
||||
// Laden des Space-Namens, wenn eine spaceId vorhanden ist
|
||||
useEffect(() => {
|
||||
const loadSpace = async () => {
|
||||
if (!spaceId) {
|
||||
setCurrentSpace(null);
|
||||
return;
|
||||
}
|
||||
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
className={`w-full min-h-[40px] text-base ${isDarkMode ? 'text-white' : 'text-black'}`}
|
||||
placeholder={selectedTemplate?.initial_question ? selectedTemplate.initial_question : placeholder}
|
||||
placeholderTextColor={isDarkMode ? '#8E8E93' : '#8E8E93'}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
multiline
|
||||
maxLength={1000}
|
||||
onSubmitEditing={() => {
|
||||
if (text.trim()) {
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
onKeyPress={handleKeyPress}
|
||||
/>
|
||||
|
||||
<View className="flex-row justify-between items-center mt-4">
|
||||
<View className="flex-row flex-wrap">
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center py-1 px-2 rounded-md mr-4 ${
|
||||
documentMode
|
||||
? 'bg-[#0A84FF]40 border border-[#0A84FF]'
|
||||
: isDarkMode
|
||||
? 'bg-[#2C2C2E] border border-[#38383A]'
|
||||
: 'bg-[#F2F2F7] border border-[#E5E5EA]'
|
||||
}`}
|
||||
onPress={() => setDocumentMode(!documentMode)}
|
||||
>
|
||||
<Ionicons
|
||||
name={documentMode ? "document" : "document-outline"}
|
||||
size={18}
|
||||
color={documentMode ? '#0A84FF' : (isDarkMode ? '#FFFFFF' : '#000000')}
|
||||
/>
|
||||
<Text className={`ml-1 ${documentMode ? 'text-[#0A84FF] font-medium' : (isDarkMode ? 'text-white' : 'text-black')}`}>
|
||||
Dokument
|
||||
</Text>
|
||||
{documentMode && (
|
||||
<Ionicons name="checkmark-circle" size={14} color="#0A84FF" style={{marginLeft: 4}} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity className="flex-row items-center mr-4">
|
||||
<Ionicons name="attach" size={20} color={isDarkMode ? '#FFFFFF' : '#000000'} />
|
||||
<Text className={`ml-1 ${isDarkMode ? 'text-white' : 'text-black'}`}>Attach</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity className="flex-row items-center mr-4">
|
||||
<Ionicons name="search" size={20} color={isDarkMode ? '#FFFFFF' : '#000000'} />
|
||||
<Text className={`ml-1 ${isDarkMode ? 'text-white' : 'text-black'}`}>Search</Text>
|
||||
</TouchableOpacity>
|
||||
try {
|
||||
const space = await getSpace(spaceId);
|
||||
setCurrentSpace(space);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden des Space:', error);
|
||||
setCurrentSpace(null);
|
||||
}
|
||||
};
|
||||
|
||||
<View className="flex-row items-center">
|
||||
<ModelDropdown
|
||||
selectedModelId={selectedModelId}
|
||||
onSelectModel={setSelectedModelId}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center px-3 py-2 rounded-full ${text.trim() ? 'bg-[#0A84FF]' : 'bg-[#0A84FF]/20'}`}
|
||||
onPress={() => {
|
||||
console.log("Senden-Button gedrückt");
|
||||
handleSend();
|
||||
}}
|
||||
disabled={!text.trim() || isCreatingConversation}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{isCreatingConversation ? (
|
||||
<View className="flex-row items-center">
|
||||
<View className="h-4 w-4 mr-1">
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
</View>
|
||||
<Text className="text-white">Wird erstellt...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="send" size={18} color={text.trim() ? '#FFFFFF' : '#0A84FF'} />
|
||||
<Text className={`ml-1 ${text.trim() ? 'text-white' : 'text-[#0A84FF]'}`}>Senden</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="mt-4">
|
||||
<View>
|
||||
<Text className={`text-sm font-medium mb-1 ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
|
||||
Vorlagen:
|
||||
</Text>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
className="flex-row"
|
||||
>
|
||||
{isLoadingTemplates ? (
|
||||
<View className={`flex-row items-center justify-center mr-2 px-3 py-1 rounded-full border ${
|
||||
isDarkMode ? 'bg-[#2C2C2E] border-[#38383A]' : 'bg-white border-[#E5E5EA]'
|
||||
}`}>
|
||||
<ActivityIndicator size="small" color={isDarkMode ? '#FFFFFF' : '#0A84FF'} style={{marginRight: 6}} />
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
Laden...
|
||||
</Text>
|
||||
</View>
|
||||
) : templates.length > 0 ? (
|
||||
templates.map((template) => (
|
||||
<TouchableOpacity
|
||||
key={template.id}
|
||||
className={`flex-row items-center mr-2 px-3 py-1 rounded-full border ${
|
||||
selectedTemplate?.id === template.id
|
||||
? isDarkMode
|
||||
? 'bg-[#0A84FF]80 border-[#0A84FF]'
|
||||
: 'bg-[#0A84FF]40 border-[#0A84FF]'
|
||||
: isDarkMode
|
||||
? 'bg-[#2C2C2E] border-[#38383A]'
|
||||
: 'bg-white border-[#E5E5EA]'
|
||||
}`}
|
||||
onPress={() => handleTemplateSelect(template)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: template.color || '#0A84FF',
|
||||
marginRight: 6
|
||||
}}
|
||||
/>
|
||||
<Text className={`text-sm ${
|
||||
selectedTemplate?.id === template.id
|
||||
? isDarkMode ? 'text-white font-medium' : 'text-[#0A84FF] font-medium'
|
||||
: isDarkMode ? 'text-white' : 'text-black'
|
||||
}`}>
|
||||
{template.name}
|
||||
</Text>
|
||||
{selectedTemplate?.id === template.id && (
|
||||
<Ionicons
|
||||
name="checkmark-circle"
|
||||
size={14}
|
||||
color={isDarkMode ? '#FFFFFF' : '#0A84FF'}
|
||||
style={{marginLeft: 4}}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center mr-2 px-3 py-1 rounded-full border ${
|
||||
isDarkMode ? 'bg-[#2C2C2E] border-[#38383A]' : 'bg-white border-[#E5E5EA]'
|
||||
}`}
|
||||
onPress={() => router.push('/templates')}
|
||||
>
|
||||
<Ionicons
|
||||
name="add-circle-outline"
|
||||
size={16}
|
||||
color={isDarkMode ? '#FFFFFF' : '#000000'}
|
||||
style={styles.chipIcon}
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
Vorlage erstellen
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center px-3 py-1 rounded-full border ${
|
||||
isDarkMode ? 'bg-[#2C2C2E] border-[#38383A]' : 'bg-white border-[#E5E5EA]'
|
||||
}`}
|
||||
onPress={() => router.push('/templates')}
|
||||
>
|
||||
<Ionicons
|
||||
name="settings-outline"
|
||||
size={16}
|
||||
color={isDarkMode ? '#FFFFFF' : '#000000'}
|
||||
style={styles.chipIcon}
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
Verwalten
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
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 (
|
||||
<View className="w-full max-w-3xl self-center px-4">
|
||||
{/* Container für den Titel mit fester Höhe - verhindert Layout-Verschiebung */}
|
||||
<View className="h-7 flex-row items-center">
|
||||
{selectedTemplate && (
|
||||
<Text
|
||||
className={`text-base font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{selectedTemplate.name}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{currentSpace && (
|
||||
<View className="ml-auto flex-row items-center">
|
||||
<Ionicons
|
||||
name="folder-open"
|
||||
size={16}
|
||||
color={colors.primary}
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
<Text
|
||||
className={`text-sm font-medium`}
|
||||
style={{ color: colors.primary }}
|
||||
numberOfLines={1}
|
||||
>
|
||||
Space: {currentSpace.name}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View className={`rounded-lg p-4 ${isDarkMode ? 'bg-[#2C2C2E]' : 'bg-white'}`}>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
className={`min-h-[40px] w-full text-base ${isDarkMode ? 'text-white' : 'text-black'}`}
|
||||
placeholder={
|
||||
selectedTemplate?.initial_question ? selectedTemplate.initial_question : placeholder
|
||||
}
|
||||
placeholderTextColor={isDarkMode ? '#8E8E93' : '#8E8E93'}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
multiline
|
||||
maxLength={1000}
|
||||
onSubmitEditing={() => {
|
||||
if (text.trim()) {
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
onKeyPress={handleKeyPress}
|
||||
/>
|
||||
|
||||
<View className="mt-4 flex-row items-center justify-between">
|
||||
<View className="flex-row flex-wrap">
|
||||
<TouchableOpacity
|
||||
className={`mr-4 flex-row items-center rounded-md px-2 py-1 ${
|
||||
documentMode
|
||||
? 'bg-[#0A84FF]40 border border-[#0A84FF]'
|
||||
: isDarkMode
|
||||
? 'border border-[#38383A] bg-[#2C2C2E]'
|
||||
: 'border border-[#E5E5EA] bg-[#F2F2F7]'
|
||||
}`}
|
||||
onPress={() => setDocumentMode(!documentMode)}
|
||||
>
|
||||
<Ionicons
|
||||
name={documentMode ? 'document' : 'document-outline'}
|
||||
size={18}
|
||||
color={documentMode ? '#0A84FF' : isDarkMode ? '#FFFFFF' : '#000000'}
|
||||
/>
|
||||
<Text
|
||||
className={`ml-1 ${documentMode ? 'font-medium text-[#0A84FF]' : isDarkMode ? 'text-white' : 'text-black'}`}
|
||||
>
|
||||
Dokument
|
||||
</Text>
|
||||
{documentMode && (
|
||||
<Ionicons
|
||||
name="checkmark-circle"
|
||||
size={14}
|
||||
color="#0A84FF"
|
||||
style={{ marginLeft: 4 }}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity className="mr-4 flex-row items-center">
|
||||
<Ionicons name="attach" size={20} color={isDarkMode ? '#FFFFFF' : '#000000'} />
|
||||
<Text className={`ml-1 ${isDarkMode ? 'text-white' : 'text-black'}`}>Attach</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity className="mr-4 flex-row items-center">
|
||||
<Ionicons name="search" size={20} color={isDarkMode ? '#FFFFFF' : '#000000'} />
|
||||
<Text className={`ml-1 ${isDarkMode ? 'text-white' : 'text-black'}`}>Search</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View className="flex-row items-center">
|
||||
<ModelDropdown
|
||||
selectedModelId={selectedModelId}
|
||||
onSelectModel={setSelectedModelId}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center rounded-full px-3 py-2 ${text.trim() ? 'bg-[#0A84FF]' : 'bg-[#0A84FF]/20'}`}
|
||||
onPress={() => {
|
||||
console.log('Senden-Button gedrückt');
|
||||
handleSend();
|
||||
}}
|
||||
disabled={!text.trim() || isCreatingConversation}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{isCreatingConversation ? (
|
||||
<View className="flex-row items-center">
|
||||
<View className="mr-1 h-4 w-4">
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
</View>
|
||||
<Text className="text-white">Wird erstellt...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="send" size={18} color={text.trim() ? '#FFFFFF' : '#0A84FF'} />
|
||||
<Text className={`ml-1 ${text.trim() ? 'text-white' : 'text-[#0A84FF]'}`}>
|
||||
Senden
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="mt-4">
|
||||
<View>
|
||||
<Text
|
||||
className={`mb-1 text-sm font-medium ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}
|
||||
>
|
||||
Vorlagen:
|
||||
</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} className="flex-row">
|
||||
{isLoadingTemplates ? (
|
||||
<View
|
||||
className={`mr-2 flex-row items-center justify-center rounded-full border px-3 py-1 ${
|
||||
isDarkMode ? 'border-[#38383A] bg-[#2C2C2E]' : 'border-[#E5E5EA] bg-white'
|
||||
}`}
|
||||
>
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={isDarkMode ? '#FFFFFF' : '#0A84FF'}
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
Laden...
|
||||
</Text>
|
||||
</View>
|
||||
) : templates.length > 0 ? (
|
||||
templates.map((template) => (
|
||||
<TouchableOpacity
|
||||
key={template.id}
|
||||
className={`mr-2 flex-row items-center rounded-full border px-3 py-1 ${
|
||||
selectedTemplate?.id === template.id
|
||||
? isDarkMode
|
||||
? 'bg-[#0A84FF]80 border-[#0A84FF]'
|
||||
: 'bg-[#0A84FF]40 border-[#0A84FF]'
|
||||
: isDarkMode
|
||||
? 'border-[#38383A] bg-[#2C2C2E]'
|
||||
: 'border-[#E5E5EA] bg-white'
|
||||
}`}
|
||||
onPress={() => handleTemplateSelect(template)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: template.color || '#0A84FF',
|
||||
marginRight: 6,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
className={`text-sm ${
|
||||
selectedTemplate?.id === template.id
|
||||
? isDarkMode
|
||||
? 'font-medium text-white'
|
||||
: 'font-medium text-[#0A84FF]'
|
||||
: isDarkMode
|
||||
? 'text-white'
|
||||
: 'text-black'
|
||||
}`}
|
||||
>
|
||||
{template.name}
|
||||
</Text>
|
||||
{selectedTemplate?.id === template.id && (
|
||||
<Ionicons
|
||||
name="checkmark-circle"
|
||||
size={14}
|
||||
color={isDarkMode ? '#FFFFFF' : '#0A84FF'}
|
||||
style={{ marginLeft: 4 }}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
className={`mr-2 flex-row items-center rounded-full border px-3 py-1 ${
|
||||
isDarkMode ? 'border-[#38383A] bg-[#2C2C2E]' : 'border-[#E5E5EA] bg-white'
|
||||
}`}
|
||||
onPress={() => router.push('/templates')}
|
||||
>
|
||||
<Ionicons
|
||||
name="add-circle-outline"
|
||||
size={16}
|
||||
color={isDarkMode ? '#FFFFFF' : '#000000'}
|
||||
style={styles.chipIcon}
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
Vorlage erstellen
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center rounded-full border px-3 py-1 ${
|
||||
isDarkMode ? 'border-[#38383A] bg-[#2C2C2E]' : 'border-[#E5E5EA] bg-white'
|
||||
}`}
|
||||
onPress={() => router.push('/templates')}
|
||||
>
|
||||
<Ionicons
|
||||
name="settings-outline"
|
||||
size={16}
|
||||
color={isDarkMode ? '#FFFFFF' : '#000000'}
|
||||
style={styles.chipIcon}
|
||||
/>
|
||||
<Text className={`text-sm ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
Verwalten
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Styles für Elemente, die nicht mit NativeWind gestylt werden können
|
||||
const styles = StyleSheet.create({
|
||||
chipIcon: {
|
||||
marginRight: 6,
|
||||
},
|
||||
chipIcon: {
|
||||
marginRight: 6,
|
||||
},
|
||||
});
|
||||
|
||||
export default ConversationStarter;
|
||||
export default ConversationStarter;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
Dimensions,
|
||||
StatusBar,
|
||||
ActivityIndicator,
|
||||
SafeAreaView,
|
||||
Platform
|
||||
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';
|
||||
|
|
@ -21,470 +21,459 @@ import { getConversations } from '../services/conversation';
|
|||
const DRAWER_WIDTH = 260; // Breite des Drawer-Menüs
|
||||
|
||||
interface CustomDrawerProps {
|
||||
isVisible: boolean;
|
||||
focusInputOnHomeNavigate?: () => void;
|
||||
onClose?: () => void;
|
||||
isVisible: boolean;
|
||||
focusInputOnHomeNavigate?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export default function CustomDrawer({
|
||||
isVisible,
|
||||
focusInputOnHomeNavigate,
|
||||
onClose
|
||||
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';
|
||||
const { colors } = useTheme();
|
||||
const { isDarkMode } = useAppTheme();
|
||||
const router = useRouter();
|
||||
const { user, signOut } = useAuth();
|
||||
|
||||
// Wenn der Drawer nicht sichtbar sein soll, gib nichts zurück
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={[
|
||||
styles.drawer,
|
||||
{
|
||||
backgroundColor: bgColor,
|
||||
width: DRAWER_WIDTH,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: separatorColor
|
||||
}
|
||||
]}
|
||||
>
|
||||
{/* Drawer-Header */}
|
||||
<View style={styles.drawerHeader}>
|
||||
<Text style={[styles.drawerTitle, { color: textColor }]}>
|
||||
Menu
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={onClose}
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.iconButton,
|
||||
hovered && { backgroundColor: colors.menuItemHover }
|
||||
]}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<Ionicons
|
||||
name="close"
|
||||
size={24}
|
||||
color={textColor}
|
||||
style={{ opacity: pressed ? 0.7 : 1 }}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Hauptaktionen */}
|
||||
<View style={styles.mainActions}>
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.mainActionButton,
|
||||
{ backgroundColor: activeColor },
|
||||
pressed && { opacity: 0.85 }
|
||||
]}
|
||||
onPress={navigateToHome}
|
||||
>
|
||||
<Ionicons name="add-circle-outline" size={20} color="white" />
|
||||
<Text style={styles.mainActionText}>Neuen Chat starten</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.mainActionButton,
|
||||
{
|
||||
backgroundColor: hovered ? colors.buttonHover : 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
|
||||
marginTop: 8
|
||||
},
|
||||
pressed && { opacity: 0.8 }
|
||||
]}
|
||||
onPress={navigateToArchive}
|
||||
>
|
||||
<Ionicons name="archive-outline" size={20} color={textColor} />
|
||||
<Text style={[styles.mainActionText, { color: textColor }]}>Archiv ansehen</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.mainActionButton,
|
||||
{
|
||||
backgroundColor: hovered ? colors.buttonHover : 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
|
||||
marginTop: 8
|
||||
},
|
||||
pressed && { opacity: 0.8 }
|
||||
]}
|
||||
onPress={() => router.push('/conversations')}
|
||||
>
|
||||
<Ionicons name="chatbubbles-outline" size={20} color={textColor} />
|
||||
<Text style={[styles.mainActionText, { color: textColor }]}>Konversationen</Text>
|
||||
</Pressable>
|
||||
const [recentChats, setRecentChats] = useState<{ id: string; title: string }[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.mainActionButton,
|
||||
{
|
||||
backgroundColor: hovered ? colors.buttonHover : 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
|
||||
marginTop: 8
|
||||
},
|
||||
pressed && { opacity: 0.8 }
|
||||
]}
|
||||
onPress={navigateToDocuments}
|
||||
>
|
||||
<Ionicons name="document-text-outline" size={20} color={textColor} />
|
||||
<Text style={[styles.mainActionText, { color: textColor }]}>Dokumente ansehen</Text>
|
||||
</Pressable>
|
||||
// Lade die letzten Chats
|
||||
useEffect(() => {
|
||||
const loadRecentChats = async () => {
|
||||
if (!user || !isVisible) return;
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.mainActionButton,
|
||||
{
|
||||
backgroundColor: hovered ? colors.buttonHover : 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
|
||||
marginTop: 8
|
||||
},
|
||||
pressed && { opacity: 0.8 }
|
||||
]}
|
||||
onPress={navigateToTemplates}
|
||||
>
|
||||
<Ionicons name="file-tray-full-outline" size={20} color={textColor} />
|
||||
<Text style={[styles.mainActionText, { color: textColor }]}>Vorlagen verwalten</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.mainActionButton,
|
||||
{
|
||||
backgroundColor: hovered ? colors.buttonHover : 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
|
||||
marginTop: 8
|
||||
},
|
||||
pressed && { opacity: 0.8 }
|
||||
]}
|
||||
onPress={() => router.push('/spaces')}
|
||||
>
|
||||
<Ionicons name="people-outline" size={20} color={textColor} />
|
||||
<Text style={[styles.mainActionText, { color: textColor }]}>Spaces</Text>
|
||||
</Pressable>
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const conversations = await getConversations(user.id);
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.mainActionButton,
|
||||
{
|
||||
backgroundColor: hovered ? colors.buttonHover : 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
|
||||
marginTop: 8
|
||||
},
|
||||
pressed && { opacity: 0.8 }
|
||||
]}
|
||||
onPress={navigateToProfile}
|
||||
>
|
||||
<Ionicons name="person-outline" size={20} color={textColor} />
|
||||
<Text style={[styles.mainActionText, { color: textColor }]}>Profil & Statistiken</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Trennlinie */}
|
||||
<View style={[styles.separator, { backgroundColor: separatorColor }]} />
|
||||
|
||||
{/* Letzte Chats */}
|
||||
<View style={styles.recentChatsHeader}>
|
||||
<Text style={[styles.recentChatsTitle, { color: textColor }]}>
|
||||
Letzte Chats
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Liste der letzten Chats */}
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" color={activeColor} />
|
||||
<Text style={[styles.loadingText, { color: textColor + '80' }]}>
|
||||
Chats werden geladen...
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView style={styles.recentChatsList}>
|
||||
{recentChats.length > 0 ? (
|
||||
recentChats.map((chat) => (
|
||||
<Pressable
|
||||
key={chat.id}
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.chatItem,
|
||||
hovered && { backgroundColor: colors.menuItemHover },
|
||||
pressed && { opacity: 0.7 }
|
||||
]}
|
||||
onPress={() => navigateToConversation(chat.id)}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<>
|
||||
<Ionicons
|
||||
name="chatbubble-ellipses-outline"
|
||||
size={20}
|
||||
color={textColor + '99'}
|
||||
style={styles.chatIcon}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.chatTitle,
|
||||
{ color: textColor }
|
||||
]}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{chat.title}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
))
|
||||
) : (
|
||||
<View style={styles.emptyChatsContainer}>
|
||||
<Text style={[styles.emptyChatsText, { color: textColor + '80' }]}>
|
||||
Keine Chats vorhanden
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
{/* Benutzerinformationen und Logout-Button */}
|
||||
<View style={styles.userSection}>
|
||||
<View style={styles.separator} />
|
||||
<View style={styles.userContainer}>
|
||||
{user && (
|
||||
<View style={styles.userInfo}>
|
||||
<Ionicons name="person-circle-outline" size={24} color={textColor} />
|
||||
<Text style={[styles.userEmail, { color: textColor }]}>
|
||||
{user.email}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.logoutButton,
|
||||
{ borderColor: separatorColor },
|
||||
hovered && { backgroundColor: colors.dangerHover },
|
||||
pressed && { opacity: 0.8 }
|
||||
]}
|
||||
onPress={() => {
|
||||
signOut().then(() => router.replace('/auth/login'));
|
||||
}}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<>
|
||||
<Ionicons name="log-out-outline" size={20} color={textColor} />
|
||||
<Text style={[styles.logoutText, { color: textColor }]}>
|
||||
Abmelden
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
// 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 (
|
||||
<SafeAreaView
|
||||
style={[
|
||||
styles.drawer,
|
||||
{
|
||||
backgroundColor: bgColor,
|
||||
width: DRAWER_WIDTH,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: separatorColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* Drawer-Header */}
|
||||
<View style={styles.drawerHeader}>
|
||||
<Text style={[styles.drawerTitle, { color: textColor }]}>Menu</Text>
|
||||
<Pressable
|
||||
onPress={onClose}
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.iconButton,
|
||||
hovered && { backgroundColor: colors.menuItemHover },
|
||||
]}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<Ionicons
|
||||
name="close"
|
||||
size={24}
|
||||
color={textColor}
|
||||
style={{ opacity: pressed ? 0.7 : 1 }}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Hauptaktionen */}
|
||||
<View style={styles.mainActions}>
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.mainActionButton,
|
||||
{ backgroundColor: activeColor },
|
||||
pressed && { opacity: 0.85 },
|
||||
]}
|
||||
onPress={navigateToHome}
|
||||
>
|
||||
<Ionicons name="add-circle-outline" size={20} color="white" />
|
||||
<Text style={styles.mainActionText}>Neuen Chat starten</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.mainActionButton,
|
||||
{
|
||||
backgroundColor: hovered ? colors.buttonHover : 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
|
||||
marginTop: 8,
|
||||
},
|
||||
pressed && { opacity: 0.8 },
|
||||
]}
|
||||
onPress={navigateToArchive}
|
||||
>
|
||||
<Ionicons name="archive-outline" size={20} color={textColor} />
|
||||
<Text style={[styles.mainActionText, { color: textColor }]}>Archiv ansehen</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.mainActionButton,
|
||||
{
|
||||
backgroundColor: hovered ? colors.buttonHover : 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
|
||||
marginTop: 8,
|
||||
},
|
||||
pressed && { opacity: 0.8 },
|
||||
]}
|
||||
onPress={() => router.push('/conversations')}
|
||||
>
|
||||
<Ionicons name="chatbubbles-outline" size={20} color={textColor} />
|
||||
<Text style={[styles.mainActionText, { color: textColor }]}>Konversationen</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.mainActionButton,
|
||||
{
|
||||
backgroundColor: hovered ? colors.buttonHover : 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
|
||||
marginTop: 8,
|
||||
},
|
||||
pressed && { opacity: 0.8 },
|
||||
]}
|
||||
onPress={navigateToDocuments}
|
||||
>
|
||||
<Ionicons name="document-text-outline" size={20} color={textColor} />
|
||||
<Text style={[styles.mainActionText, { color: textColor }]}>Dokumente ansehen</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.mainActionButton,
|
||||
{
|
||||
backgroundColor: hovered ? colors.buttonHover : 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
|
||||
marginTop: 8,
|
||||
},
|
||||
pressed && { opacity: 0.8 },
|
||||
]}
|
||||
onPress={navigateToTemplates}
|
||||
>
|
||||
<Ionicons name="file-tray-full-outline" size={20} color={textColor} />
|
||||
<Text style={[styles.mainActionText, { color: textColor }]}>Vorlagen verwalten</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.mainActionButton,
|
||||
{
|
||||
backgroundColor: hovered ? colors.buttonHover : 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
|
||||
marginTop: 8,
|
||||
},
|
||||
pressed && { opacity: 0.8 },
|
||||
]}
|
||||
onPress={() => router.push('/spaces')}
|
||||
>
|
||||
<Ionicons name="people-outline" size={20} color={textColor} />
|
||||
<Text style={[styles.mainActionText, { color: textColor }]}>Spaces</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.mainActionButton,
|
||||
{
|
||||
backgroundColor: hovered ? colors.buttonHover : 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: isDarkMode ? '#38383A' : '#D1D1D6',
|
||||
marginTop: 8,
|
||||
},
|
||||
pressed && { opacity: 0.8 },
|
||||
]}
|
||||
onPress={navigateToProfile}
|
||||
>
|
||||
<Ionicons name="person-outline" size={20} color={textColor} />
|
||||
<Text style={[styles.mainActionText, { color: textColor }]}>Profil & Statistiken</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Trennlinie */}
|
||||
<View style={[styles.separator, { backgroundColor: separatorColor }]} />
|
||||
|
||||
{/* Letzte Chats */}
|
||||
<View style={styles.recentChatsHeader}>
|
||||
<Text style={[styles.recentChatsTitle, { color: textColor }]}>Letzte Chats</Text>
|
||||
</View>
|
||||
|
||||
{/* Liste der letzten Chats */}
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" color={activeColor} />
|
||||
<Text style={[styles.loadingText, { color: textColor + '80' }]}>
|
||||
Chats werden geladen...
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView style={styles.recentChatsList}>
|
||||
{recentChats.length > 0 ? (
|
||||
recentChats.map((chat) => (
|
||||
<Pressable
|
||||
key={chat.id}
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.chatItem,
|
||||
hovered && { backgroundColor: colors.menuItemHover },
|
||||
pressed && { opacity: 0.7 },
|
||||
]}
|
||||
onPress={() => navigateToConversation(chat.id)}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<>
|
||||
<Ionicons
|
||||
name="chatbubble-ellipses-outline"
|
||||
size={20}
|
||||
color={textColor + '99'}
|
||||
style={styles.chatIcon}
|
||||
/>
|
||||
<Text
|
||||
style={[styles.chatTitle, { color: textColor }]}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{chat.title}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
))
|
||||
) : (
|
||||
<View style={styles.emptyChatsContainer}>
|
||||
<Text style={[styles.emptyChatsText, { color: textColor + '80' }]}>
|
||||
Keine Chats vorhanden
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
{/* Benutzerinformationen und Logout-Button */}
|
||||
<View style={styles.userSection}>
|
||||
<View style={styles.separator} />
|
||||
<View style={styles.userContainer}>
|
||||
{user && (
|
||||
<View style={styles.userInfo}>
|
||||
<Ionicons name="person-circle-outline" size={24} color={textColor} />
|
||||
<Text style={[styles.userEmail, { color: textColor }]}>{user.email}</Text>
|
||||
</View>
|
||||
)}
|
||||
<Pressable
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.logoutButton,
|
||||
{ borderColor: separatorColor },
|
||||
hovered && { backgroundColor: colors.dangerHover },
|
||||
pressed && { opacity: 0.8 },
|
||||
]}
|
||||
onPress={() => {
|
||||
signOut().then(() => router.replace('/auth/login'));
|
||||
}}
|
||||
>
|
||||
{({ pressed, hovered }) => (
|
||||
<>
|
||||
<Ionicons name="log-out-outline" size={20} color={textColor} />
|
||||
<Text style={[styles.logoutText, { color: textColor }]}>Abmelden</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
ActivityIndicator,
|
||||
useWindowDimensions,
|
||||
Platform,
|
||||
Alert,
|
||||
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';
|
||||
|
|
@ -17,369 +17,353 @@ 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;
|
||||
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';
|
||||
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
|
||||
document,
|
||||
isLoading = false,
|
||||
versionCount,
|
||||
onSave,
|
||||
onShowVersions,
|
||||
onNextVersion,
|
||||
onPreviousVersion,
|
||||
onDeleteVersion,
|
||||
}: DocumentPanelProps) {
|
||||
const { colors } = useTheme();
|
||||
const [content, setContent] = useState<string>(document?.content || '');
|
||||
const [editing, setEditing] = useState<boolean>(false);
|
||||
const { width } = useWindowDimensions();
|
||||
|
||||
// Aktualisiere den Content, wenn sich das Dokument ändert
|
||||
useEffect(() => {
|
||||
if (document) {
|
||||
setContent(document.content);
|
||||
}
|
||||
}, [document]);
|
||||
const { colors } = useTheme();
|
||||
const [content, setContent] = useState<string>(document?.content || '');
|
||||
const [editing, setEditing] = useState<boolean>(false);
|
||||
const { width } = useWindowDimensions();
|
||||
|
||||
const handleEdit = () => {
|
||||
setEditing(true);
|
||||
};
|
||||
// Aktualisiere den Content, wenn sich das Dokument ändert
|
||||
useEffect(() => {
|
||||
if (document) {
|
||||
setContent(document.content);
|
||||
}
|
||||
}, [document]);
|
||||
|
||||
const handleCancel = () => {
|
||||
setContent(document?.content || '');
|
||||
setEditing(false);
|
||||
};
|
||||
const handleEdit = () => {
|
||||
setEditing(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (onSave) {
|
||||
onSave(content);
|
||||
}
|
||||
setEditing(false);
|
||||
};
|
||||
const handleCancel = () => {
|
||||
setContent(document?.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 (
|
||||
<View style={styles.versionControls}>
|
||||
{/* Pfeil zurück */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.versionArrow,
|
||||
!canGoBack && styles.versionArrowDisabled
|
||||
]}
|
||||
onPress={canGoBack ? onPreviousVersion : undefined}
|
||||
disabled={!canGoBack}
|
||||
>
|
||||
<Ionicons
|
||||
name="chevron-back"
|
||||
size={16}
|
||||
color={canGoBack ? '#666' : '#CCC'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Version Badge */}
|
||||
<TouchableOpacity
|
||||
style={styles.versionBadge}
|
||||
onPress={onShowVersions}
|
||||
>
|
||||
<Text style={styles.versionText}>v{currentVersion}</Text>
|
||||
{hasMultipleVersions && (
|
||||
<Text style={styles.versionCount}>{versionCount}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Pfeil vorwärts */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.versionArrow,
|
||||
!canGoForward && styles.versionArrowDisabled
|
||||
]}
|
||||
onPress={canGoForward ? onNextVersion : undefined}
|
||||
disabled={!canGoForward}
|
||||
>
|
||||
<Ionicons
|
||||
name="chevron-forward"
|
||||
size={16}
|
||||
color={canGoForward ? '#666' : '#CCC'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
const handleSave = () => {
|
||||
if (onSave) {
|
||||
onSave(content);
|
||||
}
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colors.text }]}>Dokument</Text>
|
||||
</View>
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: colors.text }]}>
|
||||
Dokument wird geladen...
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colors.text }]}>Dokument</Text>
|
||||
{renderVersionControls()}
|
||||
<View style={styles.actions}>
|
||||
{editing ? (
|
||||
<>
|
||||
<TouchableOpacity style={styles.actionButton} onPress={handleCancel}>
|
||||
<Ionicons name="close" size={22} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.actionButton} onPress={handleSave}>
|
||||
<Ionicons name="checkmark" size={22} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{document && onDeleteVersion && versionCount > 1 && (
|
||||
<TouchableOpacity
|
||||
style={styles.actionButton}
|
||||
onPress={() => {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={22} color="#ff3b30" />
|
||||
<Text style={{fontSize: 10, color: '#ff3b30', marginLeft: 4}}>Löschen</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity style={styles.actionButton} onPress={handleEdit}>
|
||||
<Ionicons name="create-outline" size={22} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{editing ? (
|
||||
<TextInput
|
||||
style={[
|
||||
styles.editor,
|
||||
{
|
||||
color: colors.text,
|
||||
backgroundColor: colors.background,
|
||||
borderColor: colors.border
|
||||
}
|
||||
]}
|
||||
multiline
|
||||
value={content}
|
||||
onChangeText={setContent}
|
||||
autoFocus
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
) : (
|
||||
<ScrollView style={styles.contentContainer}>
|
||||
{document?.content ? (
|
||||
<Markdown
|
||||
style={{
|
||||
body: {
|
||||
color: colors.text,
|
||||
fontSize: 15,
|
||||
lineHeight: 22
|
||||
},
|
||||
heading1: {
|
||||
color: colors.text,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
paddingBottom: 8,
|
||||
marginBottom: 12
|
||||
},
|
||||
heading2: {
|
||||
color: colors.text,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border + '60',
|
||||
paddingBottom: 6,
|
||||
marginBottom: 10
|
||||
},
|
||||
heading3: { color: colors.text },
|
||||
heading4: { color: colors.text },
|
||||
heading5: { color: colors.text },
|
||||
heading6: { color: colors.text },
|
||||
paragraph: {
|
||||
color: colors.text,
|
||||
marginBottom: 12
|
||||
},
|
||||
list_item: { color: colors.text },
|
||||
blockquote: {
|
||||
backgroundColor: colors.card,
|
||||
borderLeftColor: colors.primary,
|
||||
borderLeftWidth: 4,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
marginVertical: 8
|
||||
},
|
||||
code_block: {
|
||||
backgroundColor: isDarkMode(colors) ? '#1E1E1E' : '#F5F5F5',
|
||||
padding: 10,
|
||||
borderRadius: 6
|
||||
},
|
||||
link: { color: colors.primary }
|
||||
}}
|
||||
>
|
||||
{document.content}
|
||||
</Markdown>
|
||||
) : (
|
||||
<Text style={[styles.content, { color: colors.text }]}>
|
||||
Noch kein Dokument erstellt.
|
||||
</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<View style={styles.versionControls}>
|
||||
{/* Pfeil zurück */}
|
||||
<TouchableOpacity
|
||||
style={[styles.versionArrow, !canGoBack && styles.versionArrowDisabled]}
|
||||
onPress={canGoBack ? onPreviousVersion : undefined}
|
||||
disabled={!canGoBack}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={16} color={canGoBack ? '#666' : '#CCC'} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Version Badge */}
|
||||
<TouchableOpacity style={styles.versionBadge} onPress={onShowVersions}>
|
||||
<Text style={styles.versionText}>v{currentVersion}</Text>
|
||||
{hasMultipleVersions && <Text style={styles.versionCount}>{versionCount}</Text>}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Pfeil vorwärts */}
|
||||
<TouchableOpacity
|
||||
style={[styles.versionArrow, !canGoForward && styles.versionArrowDisabled]}
|
||||
onPress={canGoForward ? onNextVersion : undefined}
|
||||
disabled={!canGoForward}
|
||||
>
|
||||
<Ionicons name="chevron-forward" size={16} color={canGoForward ? '#666' : '#CCC'} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colors.text }]}>Dokument</Text>
|
||||
</View>
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: colors.text }]}>Dokument wird geladen...</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colors.text }]}>Dokument</Text>
|
||||
{renderVersionControls()}
|
||||
<View style={styles.actions}>
|
||||
{editing ? (
|
||||
<>
|
||||
<TouchableOpacity style={styles.actionButton} onPress={handleCancel}>
|
||||
<Ionicons name="close" size={22} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.actionButton} onPress={handleSave}>
|
||||
<Ionicons name="checkmark" size={22} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{document && onDeleteVersion && versionCount > 1 && (
|
||||
<TouchableOpacity
|
||||
style={styles.actionButton}
|
||||
onPress={() => {
|
||||
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');
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={22} color="#ff3b30" />
|
||||
<Text style={{ fontSize: 10, color: '#ff3b30', marginLeft: 4 }}>Löschen</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity style={styles.actionButton} onPress={handleEdit}>
|
||||
<Ionicons name="create-outline" size={22} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{editing ? (
|
||||
<TextInput
|
||||
style={[
|
||||
styles.editor,
|
||||
{
|
||||
color: colors.text,
|
||||
backgroundColor: colors.background,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
]}
|
||||
multiline
|
||||
value={content}
|
||||
onChangeText={setContent}
|
||||
autoFocus
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
) : (
|
||||
<ScrollView style={styles.contentContainer}>
|
||||
{document?.content ? (
|
||||
<Markdown
|
||||
style={{
|
||||
body: {
|
||||
color: colors.text,
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
},
|
||||
heading1: {
|
||||
color: colors.text,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
paddingBottom: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
heading2: {
|
||||
color: colors.text,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border + '60',
|
||||
paddingBottom: 6,
|
||||
marginBottom: 10,
|
||||
},
|
||||
heading3: { color: colors.text },
|
||||
heading4: { color: colors.text },
|
||||
heading5: { color: colors.text },
|
||||
heading6: { color: colors.text },
|
||||
paragraph: {
|
||||
color: colors.text,
|
||||
marginBottom: 12,
|
||||
},
|
||||
list_item: { color: colors.text },
|
||||
blockquote: {
|
||||
backgroundColor: colors.card,
|
||||
borderLeftColor: colors.primary,
|
||||
borderLeftWidth: 4,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
marginVertical: 8,
|
||||
},
|
||||
code_block: {
|
||||
backgroundColor: isDarkMode(colors) ? '#1E1E1E' : '#F5F5F5',
|
||||
padding: 10,
|
||||
borderRadius: 6,
|
||||
},
|
||||
link: { color: colors.primary },
|
||||
}}
|
||||
>
|
||||
{document.content}
|
||||
</Markdown>
|
||||
) : (
|
||||
<Text style={[styles.content, { color: colors.text }]}>
|
||||
Noch kein Dokument erstellt.
|
||||
</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,243 +1,226 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Modal,
|
||||
SafeAreaView,
|
||||
Alert,
|
||||
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;
|
||||
isVisible: boolean;
|
||||
documents: Document[];
|
||||
onClose: () => void;
|
||||
onSelectVersion: (document: Document) => void;
|
||||
onDeleteVersion?: (document: Document) => void;
|
||||
}
|
||||
|
||||
export default function DocumentVersions({
|
||||
isVisible,
|
||||
documents,
|
||||
onClose,
|
||||
onSelectVersion,
|
||||
onDeleteVersion
|
||||
isVisible,
|
||||
documents,
|
||||
onClose,
|
||||
onSelectVersion,
|
||||
onDeleteVersion,
|
||||
}: DocumentVersionsProps) {
|
||||
const { colors } = useTheme();
|
||||
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 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 (
|
||||
<View
|
||||
key={document.id}
|
||||
style={[
|
||||
styles.versionItem,
|
||||
{ borderBottomColor: colors.border }
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={{flex: 1}}
|
||||
activeOpacity={0.6}
|
||||
onPress={() => {
|
||||
console.log('Version auswählen:', document.id);
|
||||
onSelectVersion(document);
|
||||
}}
|
||||
>
|
||||
<View style={styles.versionHeader}>
|
||||
<View style={styles.versionBadge}>
|
||||
<Text style={styles.versionNumber}>v{document.version}</Text>
|
||||
</View>
|
||||
{isLatest && (
|
||||
<View style={[styles.latestBadge, { backgroundColor: colors.primary }]}>
|
||||
<Text style={styles.latestText}>Aktuell</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
</View>
|
||||
|
||||
<Text style={[styles.versionDate, { color: colors.text + '99' }]}>
|
||||
{formatDate(document.created_at)}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
style={[styles.versionPreview, { color: colors.text }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{document.content.substring(0, 150)}
|
||||
{document.content.length > 150 ? '...' : ''}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Löschen-Button außerhalb der Touchable-Fläche für den Artikel */}
|
||||
{canDelete && onDeleteVersion && (
|
||||
<TouchableOpacity
|
||||
style={[styles.deleteSeparateButton, { backgroundColor: colors.card }]}
|
||||
activeOpacity={0.7}
|
||||
onPress={() => {
|
||||
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!");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Ionicons name="trash" size={18} color="red" />
|
||||
<Text style={styles.deleteButtonText}>Löschen</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
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 (
|
||||
<Modal
|
||||
visible={isVisible}
|
||||
animationType="slide"
|
||||
transparent={false}
|
||||
>
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={styles.closeButton}
|
||||
onPress={onClose}
|
||||
>
|
||||
<Ionicons name="close" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: colors.text }]}>Dokumentversionen</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.versionsList}>
|
||||
{documents.map((document, index) => renderVersionItem(document, index === 0))}
|
||||
|
||||
{documents.length === 0 && (
|
||||
<View style={styles.emptyState}>
|
||||
<Ionicons name="document-outline" size={48} color={colors.text + '40'} />
|
||||
<Text style={[styles.emptyText, { color: colors.text + '80' }]}>
|
||||
Keine Dokumentversionen verfügbar
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<View key={document.id} style={[styles.versionItem, { borderBottomColor: colors.border }]}>
|
||||
<TouchableOpacity
|
||||
style={{ flex: 1 }}
|
||||
activeOpacity={0.6}
|
||||
onPress={() => {
|
||||
console.log('Version auswählen:', document.id);
|
||||
onSelectVersion(document);
|
||||
}}
|
||||
>
|
||||
<View style={styles.versionHeader}>
|
||||
<View style={styles.versionBadge}>
|
||||
<Text style={styles.versionNumber}>v{document.version}</Text>
|
||||
</View>
|
||||
{isLatest && (
|
||||
<View style={[styles.latestBadge, { backgroundColor: colors.primary }]}>
|
||||
<Text style={styles.latestText}>Aktuell</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Text style={[styles.versionDate, { color: colors.text + '99' }]}>
|
||||
{formatDate(document.created_at)}
|
||||
</Text>
|
||||
|
||||
<Text style={[styles.versionPreview, { color: colors.text }]} numberOfLines={2}>
|
||||
{document.content.substring(0, 150)}
|
||||
{document.content.length > 150 ? '...' : ''}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Löschen-Button außerhalb der Touchable-Fläche für den Artikel */}
|
||||
{canDelete && onDeleteVersion && (
|
||||
<TouchableOpacity
|
||||
style={[styles.deleteSeparateButton, { backgroundColor: colors.card }]}
|
||||
activeOpacity={0.7}
|
||||
onPress={() => {
|
||||
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!');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Ionicons name="trash" size={18} color="red" />
|
||||
<Text style={styles.deleteButtonText}>Löschen</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal visible={isVisible} animationType="slide" transparent={false}>
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
||||
<Ionicons name="close" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: colors.text }]}>Dokumentversionen</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.versionsList}>
|
||||
{documents.map((document, index) => renderVersionItem(document, index === 0))}
|
||||
|
||||
{documents.length === 0 && (
|
||||
<View style={styles.emptyState}>
|
||||
<Ionicons name="document-outline" size={48} color={colors.text + '40'} />
|
||||
<Text style={[styles.emptyText, { color: colors.text + '80' }]}>
|
||||
Keine Dokumentversionen verfügbar
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,29 +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.';
|
||||
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 (
|
||||
<View>
|
||||
<View className={styles.getStartedContainer}>
|
||||
<Text className={styles.getStartedText}>{title}</Text>
|
||||
<View className={styles.codeHighlightContainer + styles.homeScreenFilename}>
|
||||
<Text>{path}</Text>
|
||||
</View>
|
||||
<Text className={styles.getStartedText}>{description}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<View>
|
||||
<View className={styles.getStartedContainer}>
|
||||
<Text className={styles.getStartedText}>{title}</Text>
|
||||
<View className={styles.codeHighlightContainer + styles.homeScreenFilename}>
|
||||
<Text>{path}</Text>
|
||||
</View>
|
||||
<Text className={styles.getStartedText}>{description}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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`,
|
||||
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`,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,105 +4,106 @@ import { Ionicons } from '@expo/vector-icons';
|
|||
import { useTheme } from '@react-navigation/native';
|
||||
|
||||
type MessageInputProps = {
|
||||
onSend: (message: string) => void;
|
||||
isLoading?: boolean;
|
||||
onSend: (message: string) => void;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
// Öffentliche Methoden über Ref
|
||||
export interface MessageInputRef {
|
||||
focus: () => void;
|
||||
focus: () => void;
|
||||
}
|
||||
|
||||
const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
function MessageInput({ onSend, isLoading = false }, ref) {
|
||||
const [message, setMessage] = useState('');
|
||||
const { colors } = useTheme();
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
|
||||
// Stellt die focus-Methode über ref zur Verfügung
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
}));
|
||||
const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(function MessageInput(
|
||||
{ onSend, isLoading = false },
|
||||
ref
|
||||
) {
|
||||
const [message, setMessage] = useState('');
|
||||
const { colors } = useTheme();
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
// Stellt die focus-Methode über ref zur Verfügung
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
style={[styles.input, { color: colors.text, backgroundColor: colors.background }]}
|
||||
placeholder="Nachricht eingeben..."
|
||||
placeholderTextColor={colors.text + '80'}
|
||||
value={message}
|
||||
onChangeText={setMessage}
|
||||
multiline
|
||||
maxLength={1000}
|
||||
editable={!isLoading}
|
||||
onSubmitEditing={handleSend}
|
||||
blurOnSubmit={false}
|
||||
onKeyPress={handleKeyPress}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[styles.sendButton, { backgroundColor: colors.primary }]}
|
||||
onPress={handleSend}
|
||||
disabled={!message.trim() || isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="#fff" size="small" />
|
||||
) : (
|
||||
<Ionicons name="send" size={20} color="#fff" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
);
|
||||
const handleSend = () => {
|
||||
if (message.trim() && !isLoading) {
|
||||
onSend(message.trim());
|
||||
setMessage('');
|
||||
}
|
||||
};
|
||||
|
||||
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',
|
||||
},
|
||||
// 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 (
|
||||
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
style={[styles.input, { color: colors.text, backgroundColor: colors.background }]}
|
||||
placeholder="Nachricht eingeben..."
|
||||
placeholderTextColor={colors.text + '80'}
|
||||
value={message}
|
||||
onChangeText={setMessage}
|
||||
multiline
|
||||
maxLength={1000}
|
||||
editable={!isLoading}
|
||||
onSubmitEditing={handleSend}
|
||||
blurOnSubmit={false}
|
||||
onKeyPress={handleKeyPress}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[styles.sendButton, { backgroundColor: colors.primary }]}
|
||||
onPress={handleSend}
|
||||
disabled={!message.trim() || isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="#fff" size="small" />
|
||||
) : (
|
||||
<Ionicons name="send" size={20} color="#fff" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
export default MessageInput;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -5,93 +5,79 @@ import SkeletonLoader from './SkeletonLoader';
|
|||
import TypingIndicator from './TypingIndicator';
|
||||
|
||||
type MessageProps = {
|
||||
text: string;
|
||||
sender: 'user' | 'ai';
|
||||
timestamp: Date;
|
||||
isLoading?: boolean;
|
||||
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 (
|
||||
<View style={[
|
||||
styles.container,
|
||||
isUser ? styles.userContainer : styles.aiContainer,
|
||||
{ backgroundColor: isUser ? colors.primary : colors.card }
|
||||
]}>
|
||||
{isLoading && sender === 'ai' ? (
|
||||
// Zeige Skeleton oder TypingIndicator wenn geladen wird
|
||||
<>
|
||||
<SkeletonLoader
|
||||
lines={4}
|
||||
style={styles.skeletonContainer}
|
||||
/>
|
||||
<TypingIndicator
|
||||
dotColor={colors.text + '80'}
|
||||
style={styles.typingIndicator}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
// Zeige die eigentliche Nachricht
|
||||
<Text style={[
|
||||
styles.messageText,
|
||||
{ color: isUser ? '#fff' : colors.text }
|
||||
]}>
|
||||
{text}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text style={[
|
||||
styles.timestamp,
|
||||
{ color: isUser ? 'rgba(255,255,255,0.7)' : colors.text + '80' }
|
||||
]}>
|
||||
{timestamp.getHours().toString().padStart(2, '0')}:{timestamp.getMinutes().toString().padStart(2, '0')}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
export default function MessageItem({ text, sender, timestamp, isLoading = false }: MessageProps) {
|
||||
const { colors } = useTheme();
|
||||
|
||||
const isUser = sender === 'user';
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
isUser ? styles.userContainer : styles.aiContainer,
|
||||
{ backgroundColor: isUser ? colors.primary : colors.card },
|
||||
]}
|
||||
>
|
||||
{isLoading && sender === 'ai' ? (
|
||||
// Zeige Skeleton oder TypingIndicator wenn geladen wird
|
||||
<>
|
||||
<SkeletonLoader lines={4} style={styles.skeletonContainer} />
|
||||
<TypingIndicator dotColor={colors.text + '80'} style={styles.typingIndicator} />
|
||||
</>
|
||||
) : (
|
||||
// Zeige die eigentliche Nachricht
|
||||
<Text style={[styles.messageText, { color: isUser ? '#fff' : colors.text }]}>{text}</Text>
|
||||
)}
|
||||
|
||||
<Text
|
||||
style={[styles.timestamp, { color: isUser ? 'rgba(255,255,255,0.7)' : colors.text + '80' }]}
|
||||
>
|
||||
{timestamp.getHours().toString().padStart(2, '0')}:
|
||||
{timestamp.getMinutes().toString().padStart(2, '0')}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,62 +3,62 @@ import { FlatList, StyleSheet, View } from 'react-native';
|
|||
import MessageItem from './MessageItem';
|
||||
|
||||
type Message = {
|
||||
id: string;
|
||||
text: string;
|
||||
sender: 'user' | 'ai';
|
||||
timestamp: Date;
|
||||
isLoading?: boolean;
|
||||
id: string;
|
||||
text: string;
|
||||
sender: 'user' | 'ai';
|
||||
timestamp: Date;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
type MessageListProps = {
|
||||
messages: Message[];
|
||||
isLoading?: boolean;
|
||||
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 (
|
||||
<MessageItem
|
||||
text={item.text}
|
||||
sender={item.sender}
|
||||
timestamp={item.timestamp}
|
||||
isLoading={shouldShowLoading || item.isLoading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
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 (
|
||||
<FlatList
|
||||
data={messages}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderMessageItem}
|
||||
style={styles.container}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
inverted={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListFooterComponent={<View style={styles.footer} />}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<MessageItem
|
||||
text={item.text}
|
||||
sender={item.sender}
|
||||
timestamp={item.timestamp}
|
||||
isLoading={shouldShowLoading || item.isLoading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={messages}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderMessageItem}
|
||||
style={styles.container}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
inverted={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListFooterComponent={<View style={styles.footer} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
maxWidth: 800,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
contentContainer: {
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
footer: {
|
||||
height: 20,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
maxWidth: 800,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
contentContainer: {
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
footer: {
|
||||
height: 20,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,118 +5,108 @@ 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
|
||||
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
|
||||
export default function ModelCard({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
isSelected = false,
|
||||
onSelect,
|
||||
model,
|
||||
}: ModelCardProps) {
|
||||
const { colors } = useTheme();
|
||||
const deployment = model?.parameters?.deployment;
|
||||
const { colors } = useTheme();
|
||||
const deployment = model?.parameters?.deployment;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderColor: isSelected ? colors.primary : 'transparent',
|
||||
}
|
||||
]}
|
||||
onPress={() => onSelect(id)}
|
||||
>
|
||||
<View style={styles.iconContainer}>
|
||||
<Ionicons
|
||||
name="chatbubble-ellipses-outline"
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.contentContainer}>
|
||||
<Text style={[styles.name, { color: colors.text }]}>{name}</Text>
|
||||
<Text
|
||||
style={[styles.description, { color: colors.text + '80' }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
|
||||
{deployment && (
|
||||
<Text
|
||||
style={[styles.deployment, { color: colors.primary + 'CC' }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{deployment}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{isSelected && (
|
||||
<View style={[styles.checkmark, { backgroundColor: colors.primary }]}>
|
||||
<Ionicons name="checkmark" size={16} color="#fff" />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
borderColor: isSelected ? colors.primary : 'transparent',
|
||||
},
|
||||
]}
|
||||
onPress={() => onSelect(id)}
|
||||
>
|
||||
<View style={styles.iconContainer}>
|
||||
<Ionicons name="chatbubble-ellipses-outline" size={24} color={colors.primary} />
|
||||
</View>
|
||||
|
||||
<View style={styles.contentContainer}>
|
||||
<Text style={[styles.name, { color: colors.text }]}>{name}</Text>
|
||||
<Text style={[styles.description, { color: colors.text + '80' }]} numberOfLines={2}>
|
||||
{description}
|
||||
</Text>
|
||||
|
||||
{deployment && (
|
||||
<Text style={[styles.deployment, { color: colors.primary + 'CC' }]} numberOfLines={1}>
|
||||
{deployment}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{isSelected && (
|
||||
<View style={[styles.checkmark, { backgroundColor: colors.primary }]}>
|
||||
<Ionicons name="checkmark" size={16} color="#fff" />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,152 +10,152 @@ import { getModels } from '../services/modelService';
|
|||
const FALLBACK_MODELS: Model[] = availableModels;
|
||||
|
||||
type ModelDropdownProps = {
|
||||
selectedModelId: string;
|
||||
onSelectModel: (id: string) => void;
|
||||
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<Model[]>(FALLBACK_MODELS);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { isDarkMode } = useAppTheme();
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [models, setModels] = useState<Model[]>(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();
|
||||
}, []);
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedModel = models.find(model => model.id === selectedModelId) || models[0];
|
||||
fetchModels();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
onPress={() => setIsModalVisible(true)}
|
||||
className={`flex-row items-center rounded-lg px-2 py-1 ${isDarkMode ? 'bg-[#2C2C2E]' : 'bg-gray-100'}`}
|
||||
>
|
||||
<Text className={`text-sm font-medium ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
{selectedModel.name}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="chevron-down"
|
||||
size={16}
|
||||
color={isDarkMode ? '#FFFFFF' : '#000000'}
|
||||
style={{ marginLeft: 4 }}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
const selectedModel = models.find((model) => model.id === selectedModelId) || models[0];
|
||||
|
||||
<Modal
|
||||
visible={isModalVisible}
|
||||
transparent={true}
|
||||
animationType="fade"
|
||||
onRequestClose={() => setIsModalVisible(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={() => setIsModalVisible(false)}
|
||||
>
|
||||
<View
|
||||
className={`mx-4 rounded-xl p-4 ${isDarkMode ? 'bg-[#1C1C1E]' : 'bg-white'}`}
|
||||
style={styles.modalContent}
|
||||
>
|
||||
<Text className={`text-lg font-bold mb-4 ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
Modell auswählen
|
||||
</Text>
|
||||
|
||||
{loading ? (
|
||||
<View className="py-4 items-center">
|
||||
<Text className={`${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||
Modelle werden geladen...
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={models}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center p-3 mb-2 rounded-lg ${
|
||||
item.id === selectedModelId
|
||||
? isDarkMode ? 'bg-blue-900/30' : 'bg-blue-100'
|
||||
: isDarkMode ? 'bg-[#2C2C2E]' : 'bg-gray-100'
|
||||
}`}
|
||||
onPress={() => {
|
||||
onSelectModel(item.id);
|
||||
setIsModalVisible(false);
|
||||
}}
|
||||
>
|
||||
<View className="w-8 h-8 rounded-full bg-blue-500/20 items-center justify-center mr-3">
|
||||
<Ionicons
|
||||
name="chatbubble-ellipses-outline"
|
||||
size={16}
|
||||
color="#0A84FF"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="flex-1">
|
||||
<Text className={`font-medium ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-xs mt-1 ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.description}
|
||||
</Text>
|
||||
{item.parameters?.deployment && (
|
||||
<Text
|
||||
className={`text-xs mt-1 ${isDarkMode ? 'text-blue-400' : 'text-blue-500'}`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.parameters.deployment}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{item.id === selectedModelId && (
|
||||
<View className="w-6 h-6 rounded-full bg-blue-500 items-center justify-center">
|
||||
<Ionicons name="checkmark" size={14} color="#FFFFFF" />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
className={`mt-3 py-3 rounded-lg items-center ${isDarkMode ? 'bg-[#0A84FF]' : 'bg-[#0A84FF]'}`}
|
||||
onPress={() => setIsModalVisible(false)}
|
||||
>
|
||||
<Text className="text-white font-medium">Schließen</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
onPress={() => setIsModalVisible(true)}
|
||||
className={`flex-row items-center rounded-lg px-2 py-1 ${isDarkMode ? 'bg-[#2C2C2E]' : 'bg-gray-100'}`}
|
||||
>
|
||||
<Text className={`text-sm font-medium ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
{selectedModel.name}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="chevron-down"
|
||||
size={16}
|
||||
color={isDarkMode ? '#FFFFFF' : '#000000'}
|
||||
style={{ marginLeft: 4 }}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Modal
|
||||
visible={isModalVisible}
|
||||
transparent={true}
|
||||
animationType="fade"
|
||||
onRequestClose={() => setIsModalVisible(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={() => setIsModalVisible(false)}
|
||||
>
|
||||
<View
|
||||
className={`mx-4 rounded-xl p-4 ${isDarkMode ? 'bg-[#1C1C1E]' : 'bg-white'}`}
|
||||
style={styles.modalContent}
|
||||
>
|
||||
<Text className={`mb-4 text-lg font-bold ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
Modell auswählen
|
||||
</Text>
|
||||
|
||||
{loading ? (
|
||||
<View className="items-center py-4">
|
||||
<Text className={`${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||
Modelle werden geladen...
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={models}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity
|
||||
className={`mb-2 flex-row items-center rounded-lg p-3 ${
|
||||
item.id === selectedModelId
|
||||
? isDarkMode
|
||||
? 'bg-blue-900/30'
|
||||
: 'bg-blue-100'
|
||||
: isDarkMode
|
||||
? 'bg-[#2C2C2E]'
|
||||
: 'bg-gray-100'
|
||||
}`}
|
||||
onPress={() => {
|
||||
onSelectModel(item.id);
|
||||
setIsModalVisible(false);
|
||||
}}
|
||||
>
|
||||
<View className="mr-3 h-8 w-8 items-center justify-center rounded-full bg-blue-500/20">
|
||||
<Ionicons name="chatbubble-ellipses-outline" size={16} color="#0A84FF" />
|
||||
</View>
|
||||
|
||||
<View className="flex-1">
|
||||
<Text className={`font-medium ${isDarkMode ? 'text-white' : 'text-black'}`}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Text
|
||||
className={`mt-1 text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.description}
|
||||
</Text>
|
||||
{item.parameters?.deployment && (
|
||||
<Text
|
||||
className={`mt-1 text-xs ${isDarkMode ? 'text-blue-400' : 'text-blue-500'}`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.parameters.deployment}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{item.id === selectedModelId && (
|
||||
<View className="h-6 w-6 items-center justify-center rounded-full bg-blue-500">
|
||||
<Ionicons name="checkmark" size={14} color="#FFFFFF" />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
className={`mt-3 items-center rounded-lg py-3 ${isDarkMode ? 'bg-[#0A84FF]' : 'bg-[#0A84FF]'}`}
|
||||
onPress={() => setIsModalVisible(false)}
|
||||
>
|
||||
<Text className="font-medium text-white">Schließen</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
modalContent: {
|
||||
maxHeight: '80%',
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
modalContent: {
|
||||
maxHeight: '80%',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,43 +4,43 @@ import { Ionicons } from '@expo/vector-icons';
|
|||
import { useTheme } from '@react-navigation/native';
|
||||
|
||||
type NewChatButtonProps = {
|
||||
onPress: () => void;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
export default function NewChatButton({ onPress }: NewChatButtonProps) {
|
||||
const { colors } = useTheme();
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.button, { backgroundColor: colors.primary }]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Ionicons name="add" size={24} color="#fff" style={styles.icon} />
|
||||
<Text style={styles.text}>Neuer Chat</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.button, { backgroundColor: colors.primary }]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Ionicons name="add" size={24} color="#fff" style={styles.icon} />
|
||||
<Text style={styles.text}>Neuer Chat</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,79 +3,75 @@ import { View, Animated, Easing, StyleSheet } from 'react-native';
|
|||
import { useTheme } from '@react-navigation/native';
|
||||
|
||||
type SkeletonLoaderProps = {
|
||||
lines?: number;
|
||||
animated?: boolean;
|
||||
style?: any;
|
||||
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));
|
||||
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]);
|
||||
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] };
|
||||
};
|
||||
// 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%' };
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
{Array.from({ length: lines }).map((_, index) => (
|
||||
<Animated.View
|
||||
key={index}
|
||||
style={[
|
||||
styles.line,
|
||||
getRandomWidth(index),
|
||||
{
|
||||
backgroundColor: colors.text + '20',
|
||||
opacity: fadeAnim,
|
||||
marginBottom: index === lines - 1 ? 0 : 8
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
// Zufällige Breite für die Zeilen dazwischen
|
||||
const widths = ['85%', '90%', '75%', '95%'];
|
||||
return { width: widths[index % widths.length] };
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
{Array.from({ length: lines }).map((_, index) => (
|
||||
<Animated.View
|
||||
key={index}
|
||||
style={[
|
||||
styles.line,
|
||||
getRandomWidth(index),
|
||||
{
|
||||
backgroundColor: colors.text + '20',
|
||||
opacity: fadeAnim,
|
||||
marginBottom: index === lines - 1 ? 0 : 8,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 16,
|
||||
maxWidth: '80%',
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
line: {
|
||||
height: 15,
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
container: {
|
||||
padding: 16,
|
||||
maxWidth: '80%',
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
line: {
|
||||
height: 15,
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,175 +6,155 @@ 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;
|
||||
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
|
||||
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;
|
||||
const { colors } = useTheme();
|
||||
const { isDarkMode } = useAppTheme();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor },
|
||||
isDefault && styles.defaultContainer
|
||||
]}
|
||||
onPress={() => onPress(id)}
|
||||
>
|
||||
{/* Farbiger Indikator am linken Rand */}
|
||||
<View style={[styles.colorIndicator, { backgroundColor: color }]} />
|
||||
|
||||
<View style={styles.content}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.name, { color: textColor }]}>{name}</Text>
|
||||
|
||||
{isDefault && (
|
||||
<View style={styles.defaultBadge}>
|
||||
<Text style={styles.defaultText}>Standard</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{description && (
|
||||
<Text
|
||||
style={[styles.description, { color: secondaryTextColor }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text
|
||||
style={[styles.prompt, { color: secondaryTextColor }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{truncatedPrompt}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Aktionen */}
|
||||
<View style={styles.actions}>
|
||||
{onSetDefault && !isDefault && (
|
||||
<TouchableOpacity
|
||||
style={styles.actionButton}
|
||||
onPress={() => onSetDefault(id)}
|
||||
>
|
||||
<Ionicons name="star-outline" size={20} color={isDarkMode ? '#FFFFFF' : '#666666'} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{onEdit && (
|
||||
<TouchableOpacity
|
||||
style={styles.actionButton}
|
||||
onPress={() => onEdit(id)}
|
||||
>
|
||||
<Ionicons name="pencil" size={20} color={isDarkMode ? '#FFFFFF' : '#666666'} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{onDelete && (
|
||||
<TouchableOpacity
|
||||
style={styles.actionButton}
|
||||
onPress={() => onDelete(id)}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={20} color={isDarkMode ? '#FFFFFF' : '#666666'} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
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 (
|
||||
<TouchableOpacity
|
||||
style={[styles.container, { backgroundColor }, isDefault && styles.defaultContainer]}
|
||||
onPress={() => onPress(id)}
|
||||
>
|
||||
{/* Farbiger Indikator am linken Rand */}
|
||||
<View style={[styles.colorIndicator, { backgroundColor: color }]} />
|
||||
|
||||
<View style={styles.content}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.name, { color: textColor }]}>{name}</Text>
|
||||
|
||||
{isDefault && (
|
||||
<View style={styles.defaultBadge}>
|
||||
<Text style={styles.defaultText}>Standard</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{description && (
|
||||
<Text style={[styles.description, { color: secondaryTextColor }]} numberOfLines={2}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text style={[styles.prompt, { color: secondaryTextColor }]} numberOfLines={2}>
|
||||
{truncatedPrompt}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Aktionen */}
|
||||
<View style={styles.actions}>
|
||||
{onSetDefault && !isDefault && (
|
||||
<TouchableOpacity style={styles.actionButton} onPress={() => onSetDefault(id)}>
|
||||
<Ionicons name="star-outline" size={20} color={isDarkMode ? '#FFFFFF' : '#666666'} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{onEdit && (
|
||||
<TouchableOpacity style={styles.actionButton} onPress={() => onEdit(id)}>
|
||||
<Ionicons name="pencil" size={20} color={isDarkMode ? '#FFFFFF' : '#666666'} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{onDelete && (
|
||||
<TouchableOpacity style={styles.actionButton} onPress={() => onDelete(id)}>
|
||||
<Ionicons name="trash-outline" size={20} color={isDarkMode ? '#FFFFFF' : '#666666'} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
Alert
|
||||
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';
|
||||
|
|
@ -18,400 +18,401 @@ 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
|
||||
'#0A84FF', // Blau
|
||||
'#32D74B', // Grün
|
||||
'#FF375F', // Rot
|
||||
'#FF9F0A', // Orange
|
||||
'#5E5CE6', // Lila
|
||||
'#BF5AF2', // Pink
|
||||
'#64D2FF', // Hellblau
|
||||
'#30D158', // Grün
|
||||
'#FF453A', // Rot
|
||||
];
|
||||
|
||||
interface TemplateFormProps {
|
||||
initialData?: Partial<Template>;
|
||||
onSubmit: (data: Partial<Template>) => void;
|
||||
onCancel: () => void;
|
||||
initialData?: Partial<Template>;
|
||||
onSubmit: (data: Partial<Template>) => 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 (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={[styles.container, { backgroundColor: bgColor }]}
|
||||
>
|
||||
<ScrollView style={styles.scrollView}>
|
||||
<View style={styles.form}>
|
||||
{/* Titel */}
|
||||
<Text style={[styles.title, { color: textColor }]}>
|
||||
{isEditMode ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen'}
|
||||
</Text>
|
||||
|
||||
{/* Name */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: textColor }]}>Name *</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
color: textColor,
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: errors.name ? '#FF3B30' : borderColor
|
||||
}
|
||||
]}
|
||||
placeholder="Name der Vorlage"
|
||||
placeholderTextColor={placeholderColor}
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
maxLength={50}
|
||||
/>
|
||||
{errors.name && (
|
||||
<Text style={styles.errorText}>{errors.name}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Beschreibung */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: textColor }]}>Beschreibung (optional)</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
styles.textArea,
|
||||
{
|
||||
color: textColor,
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor
|
||||
}
|
||||
]}
|
||||
placeholder="Kurze Beschreibung dieser Vorlage"
|
||||
placeholderTextColor={placeholderColor}
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
multiline
|
||||
numberOfLines={2}
|
||||
maxLength={200}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* System-Prompt */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: textColor }]}>System-Prompt *</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
styles.textArea,
|
||||
{
|
||||
color: textColor,
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: errors.systemPrompt ? '#FF3B30' : borderColor,
|
||||
height: 150
|
||||
}
|
||||
]}
|
||||
placeholder="System-Prompt für die KI"
|
||||
placeholderTextColor={placeholderColor}
|
||||
value={systemPrompt}
|
||||
onChangeText={setSystemPrompt}
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
{errors.systemPrompt && (
|
||||
<Text style={styles.errorText}>{errors.systemPrompt}</Text>
|
||||
)}
|
||||
<Text style={[styles.helperText, { color: isDarkMode ? '#8E8E93' : '#8A8A8E' }]}>
|
||||
Der System-Prompt definiert die Rolle und das Verhalten der KI.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Initiale Frage */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: textColor }]}>Beispielfrage (optional)</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
styles.textArea,
|
||||
{
|
||||
color: textColor,
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: borderColor,
|
||||
height: 80
|
||||
}
|
||||
]}
|
||||
placeholder="Beispiel für eine passende Frage oder Anweisung"
|
||||
placeholderTextColor={placeholderColor}
|
||||
value={initialQuestion}
|
||||
onChangeText={setInitialQuestion}
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
<Text style={[styles.helperText, { color: isDarkMode ? '#8E8E93' : '#8A8A8E' }]}>
|
||||
Diese Frage wird als Vorschlag angezeigt, wenn die Vorlage ausgewählt wird.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Farbe auswählen */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: textColor }]}>Farbe</Text>
|
||||
<View style={styles.colorPicker}>
|
||||
{TEMPLATE_COLORS.map((color) => (
|
||||
<TouchableOpacity
|
||||
key={color}
|
||||
style={[
|
||||
styles.colorOption,
|
||||
{ backgroundColor: color },
|
||||
selectedColor === color && styles.selectedColorOption
|
||||
]}
|
||||
onPress={() => setSelectedColor(color)}
|
||||
>
|
||||
{selectedColor === color && (
|
||||
<Ionicons name="checkmark" size={16} color="white" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Modell auswählen */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: textColor }]}>Bevorzugtes Modell (optional)</Text>
|
||||
<ModelDropdown
|
||||
selectedModelId={selectedModelId}
|
||||
onSelectModel={setSelectedModelId}
|
||||
/>
|
||||
<Text style={[styles.helperText, { color: isDarkMode ? '#8E8E93' : '#8A8A8E' }]}>
|
||||
Falls ausgewählt, wird dieses Modell automatisch mit der Vorlage verwendet.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Dokumentmodus */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: textColor }]}>Dokumentmodus</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.switchContainer,
|
||||
{
|
||||
backgroundColor: documentMode ? colors.primary + '20' : isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: documentMode ? colors.primary : borderColor
|
||||
}
|
||||
]}
|
||||
onPress={() => setDocumentMode(!documentMode)}
|
||||
>
|
||||
<View style={styles.switchText}>
|
||||
<Text style={[styles.switchLabel, { color: textColor }]}>
|
||||
Dokumentmodus aktivieren
|
||||
</Text>
|
||||
<Text style={[styles.switchDescription, { color: isDarkMode ? '#8E8E93' : '#8A8A8E' }]}>
|
||||
Ermöglicht die Bearbeitung eines Dokuments während der Konversation
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[
|
||||
styles.switchButton,
|
||||
{ backgroundColor: documentMode ? colors.primary : isDarkMode ? '#636366' : '#C7C7CC' }
|
||||
]}>
|
||||
{documentMode ? (
|
||||
<Ionicons name="checkmark" size={14} color="white" />
|
||||
) : (
|
||||
<Ionicons name="close" size={14} color="white" />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Buttons */}
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.cancelButton, { borderColor }]}
|
||||
onPress={onCancel}
|
||||
>
|
||||
<Text style={[styles.buttonText, { color: textColor }]}>Abbrechen</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.submitButton, { backgroundColor: colors.primary }]}
|
||||
onPress={handleSubmit}
|
||||
>
|
||||
<Text style={[styles.buttonText, { color: 'white' }]}>
|
||||
{isEditMode ? 'Speichern' : 'Erstellen'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
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 (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={[styles.container, { backgroundColor: bgColor }]}
|
||||
>
|
||||
<ScrollView style={styles.scrollView}>
|
||||
<View style={styles.form}>
|
||||
{/* Titel */}
|
||||
<Text style={[styles.title, { color: textColor }]}>
|
||||
{isEditMode ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen'}
|
||||
</Text>
|
||||
|
||||
{/* Name */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: textColor }]}>Name *</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
color: textColor,
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: errors.name ? '#FF3B30' : borderColor,
|
||||
},
|
||||
]}
|
||||
placeholder="Name der Vorlage"
|
||||
placeholderTextColor={placeholderColor}
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
maxLength={50}
|
||||
/>
|
||||
{errors.name && <Text style={styles.errorText}>{errors.name}</Text>}
|
||||
</View>
|
||||
|
||||
{/* Beschreibung */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: textColor }]}>Beschreibung (optional)</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
styles.textArea,
|
||||
{
|
||||
color: textColor,
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor,
|
||||
},
|
||||
]}
|
||||
placeholder="Kurze Beschreibung dieser Vorlage"
|
||||
placeholderTextColor={placeholderColor}
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
multiline
|
||||
numberOfLines={2}
|
||||
maxLength={200}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* System-Prompt */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: textColor }]}>System-Prompt *</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
styles.textArea,
|
||||
{
|
||||
color: textColor,
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: errors.systemPrompt ? '#FF3B30' : borderColor,
|
||||
height: 150,
|
||||
},
|
||||
]}
|
||||
placeholder="System-Prompt für die KI"
|
||||
placeholderTextColor={placeholderColor}
|
||||
value={systemPrompt}
|
||||
onChangeText={setSystemPrompt}
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
{errors.systemPrompt && <Text style={styles.errorText}>{errors.systemPrompt}</Text>}
|
||||
<Text style={[styles.helperText, { color: isDarkMode ? '#8E8E93' : '#8A8A8E' }]}>
|
||||
Der System-Prompt definiert die Rolle und das Verhalten der KI.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Initiale Frage */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: textColor }]}>Beispielfrage (optional)</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
styles.textArea,
|
||||
{
|
||||
color: textColor,
|
||||
backgroundColor: isDarkMode ? '#2C2C2E' : '#F2F2F7',
|
||||
borderColor: borderColor,
|
||||
height: 80,
|
||||
},
|
||||
]}
|
||||
placeholder="Beispiel für eine passende Frage oder Anweisung"
|
||||
placeholderTextColor={placeholderColor}
|
||||
value={initialQuestion}
|
||||
onChangeText={setInitialQuestion}
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
<Text style={[styles.helperText, { color: isDarkMode ? '#8E8E93' : '#8A8A8E' }]}>
|
||||
Diese Frage wird als Vorschlag angezeigt, wenn die Vorlage ausgewählt wird.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Farbe auswählen */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: textColor }]}>Farbe</Text>
|
||||
<View style={styles.colorPicker}>
|
||||
{TEMPLATE_COLORS.map((color) => (
|
||||
<TouchableOpacity
|
||||
key={color}
|
||||
style={[
|
||||
styles.colorOption,
|
||||
{ backgroundColor: color },
|
||||
selectedColor === color && styles.selectedColorOption,
|
||||
]}
|
||||
onPress={() => setSelectedColor(color)}
|
||||
>
|
||||
{selectedColor === color && <Ionicons name="checkmark" size={16} color="white" />}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Modell auswählen */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: textColor }]}>Bevorzugtes Modell (optional)</Text>
|
||||
<ModelDropdown selectedModelId={selectedModelId} onSelectModel={setSelectedModelId} />
|
||||
<Text style={[styles.helperText, { color: isDarkMode ? '#8E8E93' : '#8A8A8E' }]}>
|
||||
Falls ausgewählt, wird dieses Modell automatisch mit der Vorlage verwendet.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Dokumentmodus */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={[styles.label, { color: textColor }]}>Dokumentmodus</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.switchContainer,
|
||||
{
|
||||
backgroundColor: documentMode
|
||||
? colors.primary + '20'
|
||||
: isDarkMode
|
||||
? '#2C2C2E'
|
||||
: '#F2F2F7',
|
||||
borderColor: documentMode ? colors.primary : borderColor,
|
||||
},
|
||||
]}
|
||||
onPress={() => setDocumentMode(!documentMode)}
|
||||
>
|
||||
<View style={styles.switchText}>
|
||||
<Text style={[styles.switchLabel, { color: textColor }]}>
|
||||
Dokumentmodus aktivieren
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.switchDescription, { color: isDarkMode ? '#8E8E93' : '#8A8A8E' }]}
|
||||
>
|
||||
Ermöglicht die Bearbeitung eines Dokuments während der Konversation
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
styles.switchButton,
|
||||
{
|
||||
backgroundColor: documentMode
|
||||
? colors.primary
|
||||
: isDarkMode
|
||||
? '#636366'
|
||||
: '#C7C7CC',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{documentMode ? (
|
||||
<Ionicons name="checkmark" size={14} color="white" />
|
||||
) : (
|
||||
<Ionicons name="close" size={14} color="white" />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Buttons */}
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.cancelButton, { borderColor }]}
|
||||
onPress={onCancel}
|
||||
>
|
||||
<Text style={[styles.buttonText, { color: textColor }]}>Abbrechen</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.submitButton, { backgroundColor: colors.primary }]}
|
||||
onPress={handleSubmit}
|
||||
>
|
||||
<Text style={[styles.buttonText, { color: 'white' }]}>
|
||||
{isEditMode ? 'Speichern' : 'Erstellen'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,101 +3,101 @@ 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;
|
||||
dotCount?: number;
|
||||
dotSize?: number;
|
||||
dotColor?: string;
|
||||
style?: any;
|
||||
};
|
||||
|
||||
export default function TypingIndicator({
|
||||
dotCount = 3,
|
||||
dotSize = 8,
|
||||
dotColor,
|
||||
style,
|
||||
dotCount = 3,
|
||||
dotSize = 8,
|
||||
dotColor,
|
||||
style,
|
||||
}: TypingIndicatorProps) {
|
||||
const { colors } = useTheme();
|
||||
const [animations] = useState(() =>
|
||||
Array.from({ length: dotCount }).map(() => new Animated.Value(0))
|
||||
);
|
||||
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;
|
||||
// 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),
|
||||
])
|
||||
);
|
||||
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();
|
||||
};
|
||||
// Starte alle Animationen parallel und in einer Schleife
|
||||
Animated.loop(Animated.parallel(animationSequence)).start();
|
||||
};
|
||||
|
||||
animateDots();
|
||||
animateDots();
|
||||
|
||||
// Cleanup beim Unmount
|
||||
return () => {
|
||||
animations.forEach(anim => anim.stopAnimation());
|
||||
};
|
||||
}, [animations, dotCount]);
|
||||
// Cleanup beim Unmount
|
||||
return () => {
|
||||
animations.forEach((anim) => anim.stopAnimation());
|
||||
};
|
||||
}, [animations, dotCount]);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
{animations.map((anim, index) => (
|
||||
<Animated.View
|
||||
key={index}
|
||||
style={[
|
||||
styles.dot,
|
||||
{
|
||||
width: dotSize,
|
||||
height: dotSize,
|
||||
backgroundColor: actualDotColor,
|
||||
borderRadius: dotSize / 2,
|
||||
marginHorizontal: dotSize / 3,
|
||||
transform: [
|
||||
{
|
||||
translateY: anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, -dotSize],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
{animations.map((anim, index) => (
|
||||
<Animated.View
|
||||
key={index}
|
||||
style={[
|
||||
styles.dot,
|
||||
{
|
||||
width: dotSize,
|
||||
height: dotSize,
|
||||
backgroundColor: actualDotColor,
|
||||
borderRadius: dotSize / 2,
|
||||
marginHorizontal: dotSize / 3,
|
||||
transform: [
|
||||
{
|
||||
translateY: anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, -dotSize],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
dot: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
});
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
dot: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,49 +7,49 @@
|
|||
// 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',
|
||||
}
|
||||
}
|
||||
{
|
||||
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);
|
||||
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);
|
||||
return availableModels.find((m) => m.parameters.deployment === deployment);
|
||||
}
|
||||
|
||||
// Default model
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ import React, { createContext, useContext, useEffect, useState } from 'react';
|
|||
import { ActivityIndicator, View, Text } from 'react-native';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import {
|
||||
createAuthService,
|
||||
createTokenManager,
|
||||
setStorageAdapter,
|
||||
setDeviceAdapter,
|
||||
setNetworkAdapter,
|
||||
createMemoryStorageAdapter,
|
||||
type UserData,
|
||||
createAuthService,
|
||||
createTokenManager,
|
||||
setStorageAdapter,
|
||||
setDeviceAdapter,
|
||||
setNetworkAdapter,
|
||||
createMemoryStorageAdapter,
|
||||
type UserData,
|
||||
} from '@manacore/shared-auth';
|
||||
|
||||
// Mana Core Auth URL from environment
|
||||
|
|
@ -16,59 +16,59 @@ const MANA_AUTH_URL = process.env.EXPO_PUBLIC_MANA_CORE_AUTH_URL || 'http://loca
|
|||
|
||||
// Create SecureStore adapter for React Native
|
||||
const createSecureStoreAdapter = () => ({
|
||||
async getItem<T>(key: string): Promise<T | null> {
|
||||
try {
|
||||
const value = await SecureStore.getItemAsync(key);
|
||||
return value ? JSON.parse(value) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async setItem(key: string, value: unknown): Promise<void> {
|
||||
await SecureStore.setItemAsync(key, JSON.stringify(value));
|
||||
},
|
||||
async removeItem(key: string): Promise<void> {
|
||||
await SecureStore.deleteItemAsync(key);
|
||||
},
|
||||
async getItem<T>(key: string): Promise<T | null> {
|
||||
try {
|
||||
const value = await SecureStore.getItemAsync(key);
|
||||
return value ? JSON.parse(value) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async setItem(key: string, value: unknown): Promise<void> {
|
||||
await SecureStore.setItemAsync(key, JSON.stringify(value));
|
||||
},
|
||||
async removeItem(key: string): Promise<void> {
|
||||
await SecureStore.deleteItemAsync(key);
|
||||
},
|
||||
});
|
||||
|
||||
// Create device adapter for React Native
|
||||
const createReactNativeDeviceAdapter = () => {
|
||||
let deviceId: string | null = null;
|
||||
let deviceId: string | null = null;
|
||||
|
||||
return {
|
||||
async getDeviceInfo() {
|
||||
if (!deviceId) {
|
||||
// Try to get stored device ID
|
||||
deviceId = await SecureStore.getItemAsync('@device/id');
|
||||
return {
|
||||
async getDeviceInfo() {
|
||||
if (!deviceId) {
|
||||
// Try to get stored device ID
|
||||
deviceId = await SecureStore.getItemAsync('@device/id');
|
||||
|
||||
if (!deviceId) {
|
||||
// Generate new device ID
|
||||
deviceId = `rn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
await SecureStore.setItemAsync('@device/id', deviceId);
|
||||
}
|
||||
}
|
||||
if (!deviceId) {
|
||||
// Generate new device ID
|
||||
deviceId = `rn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
await SecureStore.setItemAsync('@device/id', deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
deviceId,
|
||||
deviceName: 'React Native Device',
|
||||
platform: 'react-native',
|
||||
};
|
||||
},
|
||||
async getStoredDeviceId() {
|
||||
return deviceId || (await SecureStore.getItemAsync('@device/id'));
|
||||
},
|
||||
};
|
||||
return {
|
||||
deviceId,
|
||||
deviceName: 'React Native Device',
|
||||
platform: 'react-native',
|
||||
};
|
||||
},
|
||||
async getStoredDeviceId() {
|
||||
return deviceId || (await SecureStore.getItemAsync('@device/id'));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Create network adapter (basic implementation)
|
||||
const createReactNativeNetworkAdapter = () => ({
|
||||
async isConnected() {
|
||||
return true; // Always assume connected for now
|
||||
},
|
||||
async hasStableConnection() {
|
||||
return true;
|
||||
},
|
||||
async isConnected() {
|
||||
return true; // Always assume connected for now
|
||||
},
|
||||
async hasStableConnection() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
// Initialize adapters
|
||||
|
|
@ -82,12 +82,12 @@ const tokenManager = createTokenManager(authService);
|
|||
|
||||
// Auth context type
|
||||
type AuthContextType = {
|
||||
user: UserData | 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<void>;
|
||||
resetPassword: (email: string) => Promise<{ error: any | null }>;
|
||||
user: UserData | 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<void>;
|
||||
resetPassword: (email: string) => Promise<{ error: any | null }>;
|
||||
};
|
||||
|
||||
// Create auth context
|
||||
|
|
@ -95,137 +95,137 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|||
|
||||
// Hook to access auth context
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// AuthProvider component
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<UserData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [user, setUser] = useState<UserData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Initialize auth state
|
||||
useEffect(() => {
|
||||
const initialize = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Initialize auth state
|
||||
useEffect(() => {
|
||||
const initialize = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Check if user is authenticated
|
||||
const authenticated = await authService.isAuthenticated();
|
||||
// Check if user is authenticated
|
||||
const authenticated = await authService.isAuthenticated();
|
||||
|
||||
if (authenticated) {
|
||||
const userData = await authService.getUserFromToken();
|
||||
setUser(userData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Initialisieren der Auth-Session:', error);
|
||||
setUser(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
if (authenticated) {
|
||||
const userData = await authService.getUserFromToken();
|
||||
setUser(userData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Initialisieren der Auth-Session:', error);
|
||||
setUser(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initialize();
|
||||
}, []);
|
||||
initialize();
|
||||
}, []);
|
||||
|
||||
// Sign in with email and password
|
||||
const signIn = async (email: string, password: string) => {
|
||||
try {
|
||||
console.log('Versuche Anmeldung mit:', email);
|
||||
const result = await authService.signIn(email, password);
|
||||
// Sign in with email and password
|
||||
const signIn = async (email: string, password: string) => {
|
||||
try {
|
||||
console.log('Versuche Anmeldung mit:', email);
|
||||
const result = await authService.signIn(email, password);
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Auth Fehler:', result.error);
|
||||
return { error: { message: result.error } };
|
||||
}
|
||||
if (!result.success) {
|
||||
console.error('Auth Fehler:', result.error);
|
||||
return { error: { message: result.error } };
|
||||
}
|
||||
|
||||
// Get user data from token
|
||||
const userData = await authService.getUserFromToken();
|
||||
setUser(userData);
|
||||
// Get user data from token
|
||||
const userData = await authService.getUserFromToken();
|
||||
setUser(userData);
|
||||
|
||||
console.log('Anmeldung erfolgreich:', userData?.userId);
|
||||
return { error: null };
|
||||
} catch (error: any) {
|
||||
console.error('Unerwarteter Fehler beim Anmelden:', error.message || error);
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
console.log('Anmeldung erfolgreich:', userData?.userId);
|
||||
return { error: null };
|
||||
} catch (error: any) {
|
||||
console.error('Unerwarteter Fehler beim Anmelden:', error.message || error);
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
|
||||
// Sign up with email and password
|
||||
const signUp = async (email: string, password: string) => {
|
||||
try {
|
||||
const result = await authService.signUp(email, password);
|
||||
// Sign up with email and password
|
||||
const signUp = async (email: string, password: string) => {
|
||||
try {
|
||||
const result = await authService.signUp(email, password);
|
||||
|
||||
if (!result.success) {
|
||||
return { data: null, error: { message: result.error } };
|
||||
}
|
||||
if (!result.success) {
|
||||
return { data: null, error: { message: result.error } };
|
||||
}
|
||||
|
||||
// Auto sign in after successful signup
|
||||
const signInResult = await signIn(email, password);
|
||||
// Auto sign in after successful signup
|
||||
const signInResult = await signIn(email, password);
|
||||
|
||||
if (signInResult.error) {
|
||||
return { data: null, error: signInResult.error };
|
||||
}
|
||||
if (signInResult.error) {
|
||||
return { data: null, error: signInResult.error };
|
||||
}
|
||||
|
||||
return { data: user, error: null };
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Registrieren:', error);
|
||||
return { data: null, error };
|
||||
}
|
||||
};
|
||||
return { data: user, error: null };
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Registrieren:', error);
|
||||
return { data: null, error };
|
||||
}
|
||||
};
|
||||
|
||||
// Sign out
|
||||
const signOut = async () => {
|
||||
try {
|
||||
await authService.signOut();
|
||||
setUser(null);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abmelden:', error);
|
||||
}
|
||||
};
|
||||
// Sign out
|
||||
const signOut = async () => {
|
||||
try {
|
||||
await authService.signOut();
|
||||
setUser(null);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abmelden:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Reset password
|
||||
const resetPassword = async (email: string) => {
|
||||
try {
|
||||
const result = await authService.forgotPassword(email);
|
||||
// Reset password
|
||||
const resetPassword = async (email: string) => {
|
||||
try {
|
||||
const result = await authService.forgotPassword(email);
|
||||
|
||||
if (!result.success) {
|
||||
return { error: { message: result.error } };
|
||||
}
|
||||
if (!result.success) {
|
||||
return { error: { message: result.error } };
|
||||
}
|
||||
|
||||
return { error: null };
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Zurücksetzen des Passworts:', error);
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
return { error: null };
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Zurücksetzen des Passworts:', error);
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading indicator during initialization
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<ActivityIndicator size="large" color="#0A84FF" />
|
||||
<Text style={{ marginTop: 16 }}>Authentifizierung wird initialisiert...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
// Show loading indicator during initialization
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<ActivityIndicator size="large" color="#0A84FF" />
|
||||
<Text style={{ marginTop: 16 }}>Authentifizierung wird initialisiert...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Provide auth context
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
loading,
|
||||
signIn,
|
||||
signUp,
|
||||
signOut,
|
||||
resetPassword,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
// Provide auth context
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
loading,
|
||||
signIn,
|
||||
signUp,
|
||||
signOut,
|
||||
resetPassword,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
{
|
||||
"cli": {
|
||||
"version": ">= 15.0.15",
|
||||
"appVersionSource": "remote"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {
|
||||
"autoIncrement": true
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"production": {}
|
||||
}
|
||||
"cli": {
|
||||
"version": ">= 15.0.15",
|
||||
"appVersionSource": "remote"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {
|
||||
"autoIncrement": true
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,49 +2,49 @@ import { useState } from 'react';
|
|||
import { useAppTheme } from '../theme/ThemeProvider';
|
||||
|
||||
interface UseChatInputProps {
|
||||
onSend: (message: string) => void;
|
||||
isLoading?: boolean;
|
||||
initialText?: string;
|
||||
placeholder?: string;
|
||||
maxLength?: number;
|
||||
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;
|
||||
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,
|
||||
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,
|
||||
};
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
2
apps/chat/apps/mobile/nativewind-env.d.ts
vendored
2
apps/chat/apps/mobile/nativewind-env.d.ts
vendored
|
|
@ -1,3 +1,3 @@
|
|||
/// <reference types="nativewind/types" />
|
||||
|
||||
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.
|
||||
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.
|
||||
|
|
|
|||
|
|
@ -1,64 +1,64 @@
|
|||
{
|
||||
"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"
|
||||
},
|
||||
"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",
|
||||
"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
|
||||
"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"
|
||||
},
|
||||
"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",
|
||||
"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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
module.exports = {
|
||||
printWidth: 100,
|
||||
tabWidth: 2,
|
||||
singleQuote: true,
|
||||
bracketSameLine: true,
|
||||
trailingComma: 'es5',
|
||||
printWidth: 100,
|
||||
tabWidth: 2,
|
||||
singleQuote: true,
|
||||
bracketSameLine: true,
|
||||
trailingComma: 'es5',
|
||||
|
||||
plugins: [require.resolve('prettier-plugin-tailwindcss')],
|
||||
tailwindAttributes: ['className'],
|
||||
plugins: [require.resolve('prettier-plugin-tailwindcss')],
|
||||
tailwindAttributes: ['className'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
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' });
|
||||
return Response.json({ hello: 'world' });
|
||||
}
|
||||
Server features require a custom server, which can be deployed to EAS or most other hosting providers.
|
||||
|
||||
|
|
@ -22,8 +20,10 @@ API Routes are functions that are executed on a server when a route is matched.
|
|||
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
|
||||
|
|
@ -36,11 +36,10 @@ app.json
|
|||
|
||||
Copy
|
||||
|
||||
|
||||
{
|
||||
"web": {
|
||||
"output": "server"
|
||||
}
|
||||
"web": {
|
||||
"output": "server"
|
||||
}
|
||||
}
|
||||
2
|
||||
|
||||
|
|
@ -50,9 +49,8 @@ app/hello+api.ts
|
|||
|
||||
Copy
|
||||
|
||||
|
||||
export function GET(request: Request) {
|
||||
return Response.json({ hello: 'world' });
|
||||
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.
|
||||
|
||||
|
|
@ -80,17 +78,16 @@ 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);
|
||||
const response = await fetch('/hello');
|
||||
const data = await response.json();
|
||||
alert('Hello ' + data.hello);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Button onPress={() => fetchHello()} title="Fetch hello" />;
|
||||
return <Button onPress={() => fetchHello()} title="Fetch hello" />;
|
||||
}
|
||||
Relative fetch requests automatically fetch relative to the dev server origin in development, and can be configured in production using the origin field in the app.json:
|
||||
|
||||
|
|
@ -98,16 +95,15 @@ app.json
|
|||
|
||||
Copy
|
||||
|
||||
|
||||
{
|
||||
"plugins": [
|
||||
[
|
||||
"expo-router",
|
||||
{
|
||||
"origin": "https://evanbacon.dev/"
|
||||
}
|
||||
]
|
||||
]
|
||||
"plugins": [
|
||||
[
|
||||
"expo-router",
|
||||
{
|
||||
"origin": "https://evanbacon.dev/"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
This URL can be automatically configured during EAS Builds by setting the EXPO_UNSTABLE_DEPLOY_SERVER=1 environment variable. This will trigger a versioned server deployment which sets the origin to a preview deploy URL automatically.
|
||||
|
||||
|
|
@ -123,11 +119,10 @@ app/blog/[post]+api.ts
|
|||
|
||||
Copy
|
||||
|
||||
|
||||
export async function GET(request: Request, { post }: Record<string, string>) {
|
||||
// const postId = new URL(request.url).searchParams.get('post')
|
||||
// fetch data for 'post'
|
||||
return Response.json({ ... });
|
||||
// const postId = new URL(request.url).searchParams.get('post')
|
||||
// fetch data for 'post'
|
||||
return Response.json({ ... });
|
||||
}
|
||||
Request body
|
||||
Use the request.json() function to access the request body. It automatically parses the body and returns the result.
|
||||
|
|
@ -136,11 +131,10 @@ app/validate+api.ts
|
|||
|
||||
Copy
|
||||
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
const body = await request.json();
|
||||
|
||||
return Response.json({ ... });
|
||||
return Response.json({ ... });
|
||||
}
|
||||
Request query parameters
|
||||
Query parameters can be accessed by parsing the request URL:
|
||||
|
|
@ -149,13 +143,12 @@ app/endpoint+api.ts
|
|||
|
||||
Copy
|
||||
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const post = url.searchParams.get('post');
|
||||
const url = new URL(request.url);
|
||||
const post = url.searchParams.get('post');
|
||||
|
||||
// fetch data for 'post'
|
||||
return Response.json({ ... });
|
||||
// fetch data for 'post'
|
||||
return Response.json({ ... });
|
||||
}
|
||||
Response
|
||||
Responses use the global, standard Response object.
|
||||
|
|
@ -164,9 +157,8 @@ app/demo+api.ts
|
|||
|
||||
Copy
|
||||
|
||||
|
||||
export function GET() {
|
||||
return Response.json({ hello: 'universe' });
|
||||
return Response.json({ hello: 'universe' });
|
||||
}
|
||||
Errors
|
||||
You can respond to server errors by using the Response object.
|
||||
|
|
@ -175,20 +167,19 @@ app/blog/[post].ts
|
|||
|
||||
Copy
|
||||
|
||||
|
||||
import { Request, Response } from 'expo-router/server';
|
||||
|
||||
export async function GET(request: Request, { post }: Record<string, string>) {
|
||||
if (!post) {
|
||||
return new Response('No post found', {
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
});
|
||||
}
|
||||
// fetch data for `post`
|
||||
return Response.json({ ... });
|
||||
if (!post) {
|
||||
return new Response('No post found', {
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
});
|
||||
}
|
||||
// fetch data for `post`
|
||||
return Response.json({ ... });
|
||||
}
|
||||
Making requests with an undefined method will automatically return 405: Method not allowed. If an error is thrown during the request, it will automatically return 500: Internal server error.
|
||||
|
||||
|
|
@ -196,7 +187,7 @@ Bundling
|
|||
API Routes are bundled with Expo CLI and Metro bundler. They have access to all of the language features as your client code:
|
||||
|
||||
TypeScript — types and tsconfig.json paths.
|
||||
Environment variables — server routes have access to all environment variables, not just the ones prefixed with EXPO_PUBLIC_.
|
||||
Environment variables — server routes have access to all environment variables, not just the ones prefixed with EXPO*PUBLIC*.
|
||||
Node.js standard library — ensure that you are using the correct version of Node.js locally for your server environment.
|
||||
babel.config.js and metro.config.js support — settings work across both client and server code.
|
||||
Security
|
||||
|
|
@ -240,7 +231,6 @@ Set the EXPO_UNSTABLE_DEPLOY_SERVER environment variable in your .env file. This
|
|||
|
||||
Copy
|
||||
|
||||
|
||||
EXPO_UNSTABLE_DEPLOY_SERVER=1
|
||||
4
|
||||
|
||||
|
|
@ -293,18 +283,17 @@ app.json
|
|||
|
||||
Copy
|
||||
|
||||
|
||||
{
|
||||
"expo": {
|
||||
"plugins": [
|
||||
[
|
||||
"expo-router",
|
||||
{
|
||||
"origin": "http://localhost:8081"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
"expo": {
|
||||
"plugins": [
|
||||
[
|
||||
"expo-router",
|
||||
{
|
||||
"origin": "http://localhost:8081"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
Remember to remove this origin value when deploying to production.
|
||||
|
||||
|
|
@ -363,7 +352,6 @@ server.ts
|
|||
|
||||
Copy
|
||||
|
||||
|
||||
#!/usr/bin/env node
|
||||
|
||||
const path = require('path');
|
||||
|
|
@ -386,24 +374,24 @@ app.disable('x-powered-by');
|
|||
process.env.NODE_ENV = 'production';
|
||||
|
||||
app.use(
|
||||
express.static(CLIENT_BUILD_DIR, {
|
||||
maxAge: '1h',
|
||||
extensions: ['html'],
|
||||
})
|
||||
express.static(CLIENT_BUILD_DIR, {
|
||||
maxAge: '1h',
|
||||
extensions: ['html'],
|
||||
})
|
||||
);
|
||||
|
||||
app.use(morgan('tiny'));
|
||||
|
||||
app.all(
|
||||
'*',
|
||||
createRequestHandler({
|
||||
build: SERVER_BUILD_DIR,
|
||||
})
|
||||
'\*',
|
||||
createRequestHandler({
|
||||
build: SERVER_BUILD_DIR,
|
||||
})
|
||||
);
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Express server listening on port ${port}`);
|
||||
console.log(`Express server listening on port ${port}`);
|
||||
});
|
||||
|
||||
Show More
|
||||
|
|
@ -426,11 +414,10 @@ netlify/functions/server.ts
|
|||
|
||||
Copy
|
||||
|
||||
|
||||
const { createRequestHandler } = require('@expo/server/adapter/netlify');
|
||||
|
||||
const handler = createRequestHandler({
|
||||
build: require('path').join(__dirname, '../../dist/server'),
|
||||
build: require('path').join(\_\_dirname, '../../dist/server'),
|
||||
});
|
||||
|
||||
module.exports = { handler };
|
||||
|
|
@ -442,26 +429,26 @@ netlify.toml
|
|||
|
||||
Copy
|
||||
|
||||
|
||||
[build]
|
||||
command = "expo export -p web"
|
||||
functions = "netlify/functions"
|
||||
publish = "dist/client"
|
||||
command = "expo export -p web"
|
||||
functions = "netlify/functions"
|
||||
publish = "dist/client"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/.netlify/functions/server"
|
||||
status = 404
|
||||
from = "/\*"
|
||||
to = "/.netlify/functions/server"
|
||||
status = 404
|
||||
|
||||
[functions]
|
||||
# Include everything to ensure dynamic routes can be used.
|
||||
included_files = ["dist/server/**/*"]
|
||||
|
||||
# Include everything to ensure dynamic routes can be used.
|
||||
|
||||
included_files = ["dist/server/**/*"]
|
||||
|
||||
[[headers]]
|
||||
for = "/dist/server/_expo/functions/*"
|
||||
[headers.values]
|
||||
# Set to 60 seconds as an example.
|
||||
"Cache-Control" = "public, max-age=60, s-maxage=60"
|
||||
for = "/dist/server/\_expo/functions/\*"
|
||||
[headers.values] # Set to 60 seconds as an example.
|
||||
"Cache-Control" = "public, max-age=60, s-maxage=60"
|
||||
|
||||
Show More
|
||||
3
|
||||
|
|
@ -496,42 +483,38 @@ api/index.ts
|
|||
|
||||
Copy
|
||||
|
||||
|
||||
const { createRequestHandler } = require('@expo/server/adapter/vercel');
|
||||
|
||||
module.exports = createRequestHandler({
|
||||
build: require('path').join(__dirname, '../dist/server'),
|
||||
build: require('path').join(\_\_dirname, '../dist/server'),
|
||||
});
|
||||
2
|
||||
|
||||
Create a Vercel configuration file (vercel.json) at the root of your project to redirect all requests to the server function.
|
||||
|
||||
|
||||
vercel.json v3
|
||||
|
||||
|
||||
vercel.json v2
|
||||
|
||||
vercel.json
|
||||
|
||||
Copy
|
||||
|
||||
|
||||
{
|
||||
"buildCommand": "expo export -p web",
|
||||
"outputDirectory": "dist/client",
|
||||
"functions": {
|
||||
"api/index.ts": {
|
||||
"runtime": "@vercel/node@5.1.8",
|
||||
"includeFiles": "dist/server/**"
|
||||
}
|
||||
},
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"destination": "/api/index"
|
||||
}
|
||||
]
|
||||
"buildCommand": "expo export -p web",
|
||||
"outputDirectory": "dist/client",
|
||||
"functions": {
|
||||
"api/index.ts": {
|
||||
"runtime": "@vercel/node@5.1.8",
|
||||
"includeFiles": "dist/server/\*_"
|
||||
}
|
||||
},
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/(._)",
|
||||
"destination": "/api/index"
|
||||
}
|
||||
]
|
||||
}
|
||||
The newer version of the vercel.json does not use routes and builds configuration options anymore, and serves your public assets from the dist/client output directory automatically.
|
||||
|
||||
|
|
@ -558,4 +541,4 @@ No dynamic imports
|
|||
API Routes currently work by bundling all code (minus the Node.js built-ins) into a single file. This means that you cannot use any external dependencies that are not bundled with the server. For example, a library such as sharp, which includes multiple platform binaries, cannot be used. This will be addressed in a future version.
|
||||
|
||||
ESM not supported
|
||||
The current bundling implementation opts to be more unified than flexible. This means the limitation of native not supporting ESM is carried over to API Routes. All code will be transpiled down to Common JS (require/module.exports). However, we recommend you write API Routes using ESM regardless. This will be addressed in a future version.
|
||||
The current bundling implementation opts to be more unified than flexible. This means the limitation of native not supporting ESM is carried over to API Routes. All code will be transpiled down to Common JS (require/module.exports). However, we recommend you write API Routes using ESM regardless. This will be addressed in a future version.
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ As all React components are transformed with JSX, it works with 3rd party module
|
|||
import { CustomText } from "third-party-text-component";
|
||||
|
||||
export function BoldText(props) {
|
||||
// You just need to write `className="<your styles>"`
|
||||
return <CustomText className="text-bold" {...props} />;
|
||||
// You just need to write `className="<your styles>"`
|
||||
return <CustomText className="text-bold" {...props} />;
|
||||
}
|
||||
|
||||
Styling can by dynamic and you can perform conditional logic and build up complex style objects.
|
||||
|
|
@ -53,34 +53,34 @@ Styling can by dynamic and you can perform conditional logic and build up comple
|
|||
import { Text } from "react-native";
|
||||
|
||||
export function MyText({ bold, italic, lineThrough, ...props }) {
|
||||
const classNames = [];
|
||||
const classNames = [];
|
||||
|
||||
if (bold) classNames.push("font-bold");
|
||||
if (italic) classNames.push("italic");
|
||||
if (lineThrough) classNames.push("line-through");
|
||||
if (bold) classNames.push("font-bold");
|
||||
if (italic) classNames.push("italic");
|
||||
if (lineThrough) classNames.push("line-through");
|
||||
|
||||
return <Text className={classNames.join(" ")} {...props} />;
|
||||
return <Text className={classNames.join(" ")} {...props} />;
|
||||
}
|
||||
|
||||
Manually styling components via cssInterop() and remapProps()
|
||||
By default NativeWind maps className->style, but it can handle the mapping of complex components. If you'd like to manually wrap a component, you can use cssInterop() docs. If you run into any performance issues, we suggest trying out remapProps(). While less robust in its coverage, it should lead to improved performance.
|
||||
|
||||
remapProps(FlatList, {
|
||||
className: "style",
|
||||
ListFooterComponentClassName: "ListFooterComponentStyle",
|
||||
ListHeaderComponentClassName: "ListHeaderComponentStyle",
|
||||
columnWrapperClassName: "columnWrapperStyle",
|
||||
contentContainerClassName: "contentContainerStyle",
|
||||
className: "style",
|
||||
ListFooterComponentClassName: "ListFooterComponentStyle",
|
||||
ListHeaderComponentClassName: "ListHeaderComponentStyle",
|
||||
columnWrapperClassName: "columnWrapperStyle",
|
||||
contentContainerClassName: "contentContainerStyle",
|
||||
});
|
||||
|
||||
<FlatList
|
||||
{...}
|
||||
className="bg-black"
|
||||
ListHeaderComponentClassName="bg-black text-white"
|
||||
ListFooterComponentClassName="bg-black text-white"
|
||||
columnWrapperClassName="bg-black"
|
||||
contentContainerClassName="bg-black"
|
||||
indicatorClassName="bg-black"
|
||||
{...}
|
||||
className="bg-black"
|
||||
ListHeaderComponentClassName="bg-black text-white"
|
||||
ListFooterComponentClassName="bg-black text-white"
|
||||
columnWrapperClassName="bg-black"
|
||||
contentContainerClassName="bg-black"
|
||||
indicatorClassName="bg-black"
|
||||
/>
|
||||
|
||||
And can even work with components that expect style attributes as props
|
||||
|
|
@ -89,41 +89,42 @@ import { Text } from "react-native";
|
|||
import { cssInterop } from "nativewind";
|
||||
import { Svg, Circle } from "react-native-svg";
|
||||
|
||||
/**
|
||||
* Circle uses `height`/`width` props on native and className on web
|
||||
*/
|
||||
const StyledSVG = cssInterop(Svg, {
|
||||
/\*\*
|
||||
|
||||
- Circle uses `height`/`width` props on native and className on web
|
||||
\*/
|
||||
const StyledSVG = cssInterop(Svg, {
|
||||
className: {
|
||||
target: "style",
|
||||
nativeStyleToProp: {
|
||||
height: true,
|
||||
width: true,
|
||||
},
|
||||
target: "style",
|
||||
nativeStyleToProp: {
|
||||
height: true,
|
||||
width: true,
|
||||
},
|
||||
});
|
||||
/**
|
||||
* Circle uses `fill`/`stroke`/`strokeWidth` props on native and className on web
|
||||
*/
|
||||
const StyledCircle = cssInterop(Circle, {
|
||||
},
|
||||
});
|
||||
/\*\*
|
||||
- Circle uses `fill`/`stroke`/`strokeWidth` props on native and className on web
|
||||
\*/
|
||||
const StyledCircle = cssInterop(Circle, {
|
||||
className: {
|
||||
target: "style",
|
||||
nativeStyleToProp: {
|
||||
fill: true,
|
||||
stroke: true,
|
||||
strokeWidth: true,
|
||||
},
|
||||
target: "style",
|
||||
nativeStyleToProp: {
|
||||
fill: true,
|
||||
stroke: true,
|
||||
strokeWidth: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export function BoldText(props) {
|
||||
return (
|
||||
<Svg className="w-1/2 h-1/2" viewBox="0 0 100 100">
|
||||
<StyledCircle
|
||||
return (
|
||||
<Svg className="w-1/2 h-1/2" viewBox="0 0 100 100">
|
||||
<StyledCircle
|
||||
className="fill-green-500 stroke-blue-500 stroke-2"
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="45"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Eine moderne Chat-Anwendung mit LLM-Integration, entwickelt mit Expo React Nativ
|
|||
## Projektübersicht
|
||||
|
||||
Diese Anwendung ermöglicht Benutzern, mit verschiedenen LLM-Modellen zu interagieren. Sie unterstützt verschiedene Konversationsmodi:
|
||||
|
||||
- **Freier Modus**: Offene Gespräche mit dem KI-Modell
|
||||
- **Geführter Modus**: Strukturierte Konversationen mit vordefinierten Schritten
|
||||
- **Vorlagen-Modus**: Vordefinierte Gesprächsabläufe für spezifische Anwendungsfälle
|
||||
|
|
@ -14,53 +15,58 @@ Diese Anwendung ermöglicht Benutzern, mit verschiedenen LLM-Modellen zu interag
|
|||
Die Anwendung verwendet Supabase mit folgender Datenbankstruktur:
|
||||
|
||||
### Users
|
||||
| Feld | Typ | Beschreibung |
|
||||
|------|-----|-------------|
|
||||
| id | UUID (PK) | Eindeutige Benutzer-ID |
|
||||
| email | String | E-Mail-Adresse des Benutzers |
|
||||
| name | String | Name des Benutzers |
|
||||
| created_at | Timestamp | Erstellungszeitpunkt |
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
| ---------- | --------- | -------------------------------- |
|
||||
| id | UUID (PK) | Eindeutige Benutzer-ID |
|
||||
| email | String | E-Mail-Adresse des Benutzers |
|
||||
| name | String | Name des Benutzers |
|
||||
| created_at | Timestamp | Erstellungszeitpunkt |
|
||||
| updated_at | Timestamp | Letzter Aktualisierungszeitpunkt |
|
||||
|
||||
### Conversations
|
||||
| Feld | Typ | Beschreibung |
|
||||
|------|-----|-------------|
|
||||
| id | UUID (PK) | Eindeutige Konversations-ID |
|
||||
| user_id | UUID (FK) | Referenz zum Benutzer |
|
||||
| model_id | UUID (FK) | Referenz zum verwendeten LLM-Modell |
|
||||
| template_id | UUID (FK, Nullable) | Optionale Referenz zur verwendeten Vorlage |
|
||||
| conversation_mode | String | Modus der Konversation (frei, geführt, vorlage) |
|
||||
| created_at | Timestamp | Erstellungszeitpunkt |
|
||||
| updated_at | Timestamp | Letzter Aktualisierungszeitpunkt |
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
| ----------------- | ------------------- | ----------------------------------------------- |
|
||||
| id | UUID (PK) | Eindeutige Konversations-ID |
|
||||
| user_id | UUID (FK) | Referenz zum Benutzer |
|
||||
| model_id | UUID (FK) | Referenz zum verwendeten LLM-Modell |
|
||||
| template_id | UUID (FK, Nullable) | Optionale Referenz zur verwendeten Vorlage |
|
||||
| conversation_mode | String | Modus der Konversation (frei, geführt, vorlage) |
|
||||
| created_at | Timestamp | Erstellungszeitpunkt |
|
||||
| updated_at | Timestamp | Letzter Aktualisierungszeitpunkt |
|
||||
|
||||
### Messages
|
||||
| Feld | Typ | Beschreibung |
|
||||
|------|-----|-------------|
|
||||
| id | UUID (PK) | Eindeutige Nachrichten-ID |
|
||||
| conversation_id | UUID (FK) | Referenz zur Konversation |
|
||||
| sender | Enum | Absender der Nachricht (user, ai) |
|
||||
| message_text | Text | Inhalt der Nachricht |
|
||||
| created_at | Timestamp | Erstellungszeitpunkt |
|
||||
| updated_at | Timestamp | Letzter Aktualisierungszeitpunkt |
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
| --------------- | --------- | --------------------------------- |
|
||||
| id | UUID (PK) | Eindeutige Nachrichten-ID |
|
||||
| conversation_id | UUID (FK) | Referenz zur Konversation |
|
||||
| sender | Enum | Absender der Nachricht (user, ai) |
|
||||
| message_text | Text | Inhalt der Nachricht |
|
||||
| created_at | Timestamp | Erstellungszeitpunkt |
|
||||
| updated_at | Timestamp | Letzter Aktualisierungszeitpunkt |
|
||||
|
||||
### Models
|
||||
| Feld | Typ | Beschreibung |
|
||||
|------|-----|-------------|
|
||||
| id | UUID (PK) | Eindeutige Modell-ID |
|
||||
| name | String | Name des Modells (z.B. GPT-4, GPT-3) |
|
||||
| description | Text | Beschreibung des Modells |
|
||||
| parameters | JSON | Optionale Einstellungen für das Modell |
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
| ----------- | --------- | -------------------------------------- |
|
||||
| id | UUID (PK) | Eindeutige Modell-ID |
|
||||
| name | String | Name des Modells (z.B. GPT-4, GPT-3) |
|
||||
| description | Text | Beschreibung des Modells |
|
||||
| parameters | JSON | Optionale Einstellungen für das Modell |
|
||||
|
||||
### Templates
|
||||
| Feld | Typ | Beschreibung |
|
||||
|------|-----|-------------|
|
||||
| id | UUID (PK) | Eindeutige Vorlagen-ID |
|
||||
| name | String | Name der Vorlage (z.B. "Vertrag erstellen") |
|
||||
| description | Text | Beschreibung der Vorlage |
|
||||
| mode_type | Text | Detaillierte Beschreibung des Modus/Guided Flow |
|
||||
| initial_questions | JSON/Text | Startfragen oder Anweisungen |
|
||||
| created_at | Timestamp | Erstellungszeitpunkt |
|
||||
| updated_at | Timestamp | Letzter Aktualisierungszeitpunkt |
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
| ----------------- | --------- | ----------------------------------------------- |
|
||||
| id | UUID (PK) | Eindeutige Vorlagen-ID |
|
||||
| name | String | Name der Vorlage (z.B. "Vertrag erstellen") |
|
||||
| description | Text | Beschreibung der Vorlage |
|
||||
| mode_type | Text | Detaillierte Beschreibung des Modus/Guided Flow |
|
||||
| initial_questions | JSON/Text | Startfragen oder Anweisungen |
|
||||
| created_at | Timestamp | Erstellungszeitpunkt |
|
||||
| updated_at | Timestamp | Letzter Aktualisierungszeitpunkt |
|
||||
|
||||
## Beziehungen
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ Diese Dokumentation beschreibt die Integration von Supabase in die Chat-Anwendun
|
|||
## Übersicht
|
||||
|
||||
Die Chat-Anwendung verwendet Supabase als Backend-Dienst für:
|
||||
|
||||
- Benutzerauthentifizierung
|
||||
- Datenspeicherung (Konversationen, Nachrichten, Modelle)
|
||||
- Echtzeit-Updates
|
||||
|
|
@ -22,13 +23,13 @@ Die Chat-Anwendung verwendet Supabase als Backend-Dienst für:
|
|||
|
||||
Die Anwendung verwendet folgende Tabellen in Supabase:
|
||||
|
||||
| Tabelle | Beschreibung | Wichtige Felder |
|
||||
|---------|--------------|-----------------|
|
||||
| users | Benutzerinformationen | id, email, name, created_at, updated_at |
|
||||
| conversations | Gespeicherte Konversationen | id, user_id, model_id, template_id, conversation_mode, created_at, updated_at |
|
||||
| messages | Nachrichten innerhalb von Konversationen | id, conversation_id, sender, message_text, created_at, updated_at |
|
||||
| models | Verfügbare LLM-Modelle | id, name, description, parameters, created_at, updated_at |
|
||||
| templates | Konversationsvorlagen | id, name, description, mode_type, initial_questions, created_at, updated_at |
|
||||
| Tabelle | Beschreibung | Wichtige Felder |
|
||||
| ------------- | ---------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| users | Benutzerinformationen | id, email, name, created_at, updated_at |
|
||||
| conversations | Gespeicherte Konversationen | id, user_id, model_id, template_id, conversation_mode, created_at, updated_at |
|
||||
| messages | Nachrichten innerhalb von Konversationen | id, conversation_id, sender, message_text, created_at, updated_at |
|
||||
| models | Verfügbare LLM-Modelle | id, name, description, parameters, created_at, updated_at |
|
||||
| templates | Konversationsvorlagen | id, name, description, mode_type, initial_questions, created_at, updated_at |
|
||||
|
||||
## Einrichtung
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ npm run supabase:setup
|
|||
```
|
||||
|
||||
Dieses Skript:
|
||||
|
||||
- Erstellt die notwendigen Funktionen in der Datenbank
|
||||
- Richtet die Tabellen ein, falls sie noch nicht existieren
|
||||
- Fügt die Standard-Modelle mit korrekten UUIDs ein
|
||||
|
|
@ -87,6 +89,7 @@ npm run supabase:cli
|
|||
```
|
||||
|
||||
Mit diesem Tool kannst du:
|
||||
|
||||
- SQL-Abfragen direkt ausführen
|
||||
- SQL-Dateien ausführen
|
||||
- Tabellenlisten anzeigen
|
||||
|
|
@ -95,17 +98,20 @@ Mit diesem Tool kannst du:
|
|||
#### Beispiel-Befehle in der CLI
|
||||
|
||||
**Tabellenliste anzeigen:**
|
||||
|
||||
```
|
||||
3 [Tabellenliste anzeigen]
|
||||
```
|
||||
|
||||
**Tabellenstruktur anzeigen:**
|
||||
|
||||
```
|
||||
4 [Tabellenstruktur anzeigen]
|
||||
conversations
|
||||
```
|
||||
|
||||
**SQL-Abfrage ausführen:**
|
||||
|
||||
```
|
||||
1 [SQL-Abfrage ausführen]
|
||||
SELECT * FROM models LIMIT 5;
|
||||
|
|
@ -117,17 +123,22 @@ SELECT * FROM models LIMIT 5;
|
|||
|
||||
**Symptom:** Fehler "invalid input syntax for type uuid" beim Erstellen einer Konversation.
|
||||
|
||||
**Lösung:**
|
||||
**Lösung:**
|
||||
|
||||
1. Führe das Modell-Update-Skript aus, um die Modell-IDs zu korrigieren:
|
||||
|
||||
```bash
|
||||
npm run supabase:update-models
|
||||
```
|
||||
|
||||
2. Überprüfe, ob die Modell-IDs im Frontend mit denen in der Datenbank übereinstimmen:
|
||||
|
||||
```bash
|
||||
npm run supabase:cli
|
||||
```
|
||||
|
||||
Dann wähle Option 1 und führe aus:
|
||||
|
||||
```sql
|
||||
SELECT id, name FROM models;
|
||||
```
|
||||
|
|
@ -137,11 +148,15 @@ SELECT id, name FROM models;
|
|||
**Symptom:** 400 Bad Request bei der Anmeldung.
|
||||
|
||||
**Lösung:**
|
||||
|
||||
1. Überprüfe, ob der Benutzer in der Auth-Tabelle existiert:
|
||||
|
||||
```bash
|
||||
npm run supabase:cli
|
||||
```
|
||||
|
||||
Dann wähle Option 1 und führe aus:
|
||||
|
||||
```sql
|
||||
SELECT * FROM auth.users WHERE email = 'deine-email@beispiel.de';
|
||||
```
|
||||
|
|
@ -154,6 +169,7 @@ SELECT * FROM auth.users WHERE email = 'deine-email@beispiel.de';
|
|||
|
||||
1. Füge das Modell zur `FALLBACK_MODELS`-Liste in `app/api/models+api.ts` hinzu
|
||||
2. Führe das Modell-Update-Skript aus:
|
||||
|
||||
```bash
|
||||
npm run supabase:update-models
|
||||
```
|
||||
|
|
@ -162,23 +178,30 @@ npm run supabase:update-models
|
|||
|
||||
1. Erstelle eine SQL-Datei mit den gewünschten Änderungen
|
||||
2. Führe die Datei mit der Supabase-CLI aus:
|
||||
|
||||
```bash
|
||||
npm run supabase:cli
|
||||
```
|
||||
|
||||
Dann wähle Option 2 und gib den Pfad zur SQL-Datei ein.
|
||||
|
||||
### Wie kann ich die Datenbank zurücksetzen?
|
||||
|
||||
1. Verwende die Supabase-CLI:
|
||||
|
||||
```bash
|
||||
npm run supabase:cli
|
||||
```
|
||||
|
||||
2. Wähle Option 1 und führe folgende Befehle aus:
|
||||
|
||||
```sql
|
||||
DELETE FROM messages;
|
||||
DELETE FROM conversations;
|
||||
```
|
||||
|
||||
3. Führe das Modell-Update-Skript aus, um die Standard-Modelle wiederherzustellen:
|
||||
|
||||
```bash
|
||||
npm run supabase:update-models
|
||||
```
|
||||
|
|
|
|||
|
|
@ -41,10 +41,11 @@ ALTER TABLE public.models ADD COLUMN cost_settings JSONB DEFAULT '{"prompt_per_1
|
|||
```
|
||||
|
||||
Beispiel für `cost_settings`:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt_per_1k_tokens": 0.003, // Kosten pro 1000 Prompt-Tokens in Dollar
|
||||
"completion_per_1k_tokens": 0.006 // Kosten pro 1000 Completion-Tokens in Dollar
|
||||
"prompt_per_1k_tokens": 0.003, // Kosten pro 1000 Prompt-Tokens in Dollar
|
||||
"completion_per_1k_tokens": 0.006 // Kosten pro 1000 Completion-Tokens in Dollar
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -52,7 +53,7 @@ Beispiel für `cost_settings`:
|
|||
|
||||
### Erfassung der Token-Nutzung
|
||||
|
||||
1. **API-Aufruf**:
|
||||
1. **API-Aufruf**:
|
||||
- Jeder Aufruf eines LLM-Modells über `services/openai.ts` gibt Token-Nutzungsinformationen zurück
|
||||
- Diese werden aus der API-Antwort in `utils/api.ts` extrahiert
|
||||
|
||||
|
|
@ -62,12 +63,12 @@ Beispiel für `cost_settings`:
|
|||
|
||||
```typescript
|
||||
export async function logTokenUsage(
|
||||
usage: TokenUsage,
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
userId: string,
|
||||
modelId: string
|
||||
): Promise<void>
|
||||
usage: TokenUsage,
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
userId: string,
|
||||
modelId: string
|
||||
): Promise<void>;
|
||||
```
|
||||
|
||||
3. **Kostenberechnung**:
|
||||
|
|
@ -76,10 +77,10 @@ export async function logTokenUsage(
|
|||
|
||||
```typescript
|
||||
export async function calculateTokenCost(
|
||||
promptTokens: number,
|
||||
completionTokens: number,
|
||||
modelId: string
|
||||
): Promise<number>
|
||||
promptTokens: number,
|
||||
completionTokens: number,
|
||||
modelId: string
|
||||
): Promise<number>;
|
||||
```
|
||||
|
||||
### Abfrage und Analyse
|
||||
|
|
@ -87,22 +88,28 @@ export async function calculateTokenCost(
|
|||
Die folgenden SQL-Funktionen sind für die Abfrage der Token-Nutzung verfügbar:
|
||||
|
||||
1. **Nutzung nach Modell**:
|
||||
|
||||
```sql
|
||||
SELECT * FROM get_user_model_usage(user_id);
|
||||
```
|
||||
|
||||
- Gibt die Summe der Token und Kosten pro Modell für einen Benutzer zurück
|
||||
|
||||
2. **Nutzung nach Zeitraum**:
|
||||
|
||||
```sql
|
||||
SELECT * FROM get_user_usage_by_period(user_id, 'day');
|
||||
```
|
||||
|
||||
- Akzeptiert 'day', 'month' oder 'year' als Zeitraum
|
||||
- Gibt die Summe der Token und Kosten pro Zeiteinheit zurück
|
||||
|
||||
3. **Nutzung pro Konversation**:
|
||||
|
||||
```sql
|
||||
SELECT * FROM get_conversation_usage(conversation_id);
|
||||
```
|
||||
|
||||
- Gibt die Token-Nutzung für jede Nachricht in einer Konversation zurück
|
||||
|
||||
## API-Endpunkte
|
||||
|
|
@ -121,14 +128,14 @@ Die API-Endpunkte für den Zugriff auf die Token-Nutzungsdaten sind:
|
|||
|
||||
Die Standardpreise für verschiedene Modelle sind:
|
||||
|
||||
| Modell | Prompt-Tokens (pro 1K) | Completion-Tokens (pro 1K) |
|
||||
|--------|------------------------|----------------------------|
|
||||
| GPT-O3-Mini | $0.0001 | $0.0002 |
|
||||
| GPT-4o-mini | $0.0001 | $0.0002 |
|
||||
| GPT-4o | $0.003 | $0.006 |
|
||||
| GPT-4 | $0.003 | $0.006 |
|
||||
| GPT-3.5 | $0.0001 | $0.0002 |
|
||||
| Claude | $0.0008 | $0.0024 |
|
||||
| Modell | Prompt-Tokens (pro 1K) | Completion-Tokens (pro 1K) |
|
||||
| ----------- | ---------------------- | -------------------------- |
|
||||
| GPT-O3-Mini | $0.0001 | $0.0002 |
|
||||
| GPT-4o-mini | $0.0001 | $0.0002 |
|
||||
| GPT-4o | $0.003 | $0.006 |
|
||||
| GPT-4 | $0.003 | $0.006 |
|
||||
| GPT-3.5 | $0.0001 | $0.0002 |
|
||||
| Claude | $0.0008 | $0.0024 |
|
||||
|
||||
## Verwendungsbeispiele
|
||||
|
||||
|
|
@ -140,9 +147,9 @@ const { data } = await supabase.rpc('get_user_model_usage', { user_id: userId })
|
|||
console.log('Token-Nutzung nach Modell:', data);
|
||||
|
||||
// Beispiel: Abfrage der Token-Nutzung nach Monat
|
||||
const { data } = await supabase.rpc('get_user_usage_by_period', {
|
||||
user_id: userId,
|
||||
period: 'month'
|
||||
const { data } = await supabase.rpc('get_user_usage_by_period', {
|
||||
user_id: userId,
|
||||
period: 'month',
|
||||
});
|
||||
console.log('Monatliche Token-Nutzung:', data);
|
||||
```
|
||||
|
|
@ -187,4 +194,4 @@ Die Token-Nutzungsdaten können in Abrechnungssysteme integriert werden:
|
|||
|
||||
- Berechnung von Kosten für verschiedene Benutzer oder Teams
|
||||
- Integration mit Stripe oder anderen Zahlungsabwicklern
|
||||
- Implementierung von unterschiedlichen Preismodellen (Flatrate, Pay-per-Use, etc.)
|
||||
- Implementierung von unterschiedlichen Preismodellen (Flatrate, Pay-per-Use, etc.)
|
||||
|
|
|
|||
|
|
@ -16,51 +16,53 @@ const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
|
|||
const supabaseKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseKey) {
|
||||
console.error('Fehler: EXPO_PUBLIC_SUPABASE_URL und EXPO_PUBLIC_SUPABASE_ANON_KEY müssen in der .env-Datei definiert sein.');
|
||||
process.exit(1);
|
||||
console.error(
|
||||
'Fehler: EXPO_PUBLIC_SUPABASE_URL und EXPO_PUBLIC_SUPABASE_ANON_KEY müssen in der .env-Datei definiert sein.'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||
|
||||
// Funktion zum Ausführen einer SQL-Datei
|
||||
async function executeSqlFile(filePath) {
|
||||
try {
|
||||
const fullPath = join(__dirname, filePath);
|
||||
console.log(`Führe SQL-Datei aus: ${fullPath}`);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.error(`Fehler: Datei ${fullPath} existiert nicht.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const query = fs.readFileSync(fullPath, 'utf8');
|
||||
|
||||
// Teile die Abfrage in einzelne Anweisungen auf
|
||||
const statements = query.split(';').filter(stmt => stmt.trim() !== '');
|
||||
|
||||
for (const statement of statements) {
|
||||
console.log(`Führe aus: ${statement.trim()}`);
|
||||
const { error } = await supabase.rpc('execute_sql', { query: statement.trim() });
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler bei der Ausführung:', error.message);
|
||||
// Fahre trotz Fehler fort
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Lesen oder Ausführen der Datei:', error.message);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const fullPath = join(__dirname, filePath);
|
||||
console.log(`Führe SQL-Datei aus: ${fullPath}`);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.error(`Fehler: Datei ${fullPath} existiert nicht.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const query = fs.readFileSync(fullPath, 'utf8');
|
||||
|
||||
// Teile die Abfrage in einzelne Anweisungen auf
|
||||
const statements = query.split(';').filter((stmt) => stmt.trim() !== '');
|
||||
|
||||
for (const statement of statements) {
|
||||
console.log(`Führe aus: ${statement.trim()}`);
|
||||
const { error } = await supabase.rpc('execute_sql', { query: statement.trim() });
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler bei der Ausführung:', error.message);
|
||||
// Fahre trotz Fehler fort
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Lesen oder Ausführen der Datei:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Funktion zum Erstellen der execute_sql-Funktion
|
||||
async function createExecuteSqlFunction() {
|
||||
try {
|
||||
console.log('Erstelle execute_sql-Funktion...');
|
||||
|
||||
const query = `
|
||||
try {
|
||||
console.log('Erstelle execute_sql-Funktion...');
|
||||
|
||||
const query = `
|
||||
CREATE OR REPLACE FUNCTION execute_sql(query text)
|
||||
RETURNS JSONB
|
||||
LANGUAGE plpgsql
|
||||
|
|
@ -77,60 +79,58 @@ async function createExecuteSqlFunction() {
|
|||
END;
|
||||
$$;
|
||||
`;
|
||||
|
||||
const { error } = await supabase.rpc('execute_sql', { query });
|
||||
|
||||
if (error) {
|
||||
// Die Funktion existiert möglicherweise noch nicht, versuche direkte SQL-Ausführung
|
||||
console.log('Versuche direkte SQL-Ausführung...');
|
||||
|
||||
const { error: directError } = await supabase
|
||||
.from('_exec_sql')
|
||||
.insert({ sql: query });
|
||||
|
||||
if (directError) {
|
||||
console.error('Fehler beim Erstellen der execute_sql-Funktion:', directError.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('execute_sql-Funktion erfolgreich erstellt.');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Unerwarteter Fehler:', error.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
const { error } = await supabase.rpc('execute_sql', { query });
|
||||
|
||||
if (error) {
|
||||
// Die Funktion existiert möglicherweise noch nicht, versuche direkte SQL-Ausführung
|
||||
console.log('Versuche direkte SQL-Ausführung...');
|
||||
|
||||
const { error: directError } = await supabase.from('_exec_sql').insert({ sql: query });
|
||||
|
||||
if (directError) {
|
||||
console.error('Fehler beim Erstellen der execute_sql-Funktion:', directError.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('execute_sql-Funktion erfolgreich erstellt.');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Unerwarteter Fehler:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Hauptfunktion
|
||||
async function setupSupabase() {
|
||||
console.log('Starte Supabase-Setup...');
|
||||
|
||||
// Erstelle die execute_sql-Funktion
|
||||
const execSqlCreated = await createExecuteSqlFunction();
|
||||
|
||||
if (!execSqlCreated) {
|
||||
console.log('Konnte execute_sql-Funktion nicht erstellen. Versuche trotzdem fortzufahren...');
|
||||
}
|
||||
|
||||
// Führe die SQL-Dateien aus
|
||||
console.log('Führe Supabase-Funktionen-Setup aus...');
|
||||
await executeSqlFile('setup_supabase_functions.sql');
|
||||
|
||||
console.log('Führe Modell-Updates aus...');
|
||||
await executeSqlFile('update_models.sql');
|
||||
|
||||
console.log('Richte RLS-Richtlinien ein...');
|
||||
await executeSqlFile('setup_rls_policies.sql');
|
||||
|
||||
console.log('Supabase-Setup abgeschlossen.');
|
||||
console.log('Starte Supabase-Setup...');
|
||||
|
||||
// Erstelle die execute_sql-Funktion
|
||||
const execSqlCreated = await createExecuteSqlFunction();
|
||||
|
||||
if (!execSqlCreated) {
|
||||
console.log('Konnte execute_sql-Funktion nicht erstellen. Versuche trotzdem fortzufahren...');
|
||||
}
|
||||
|
||||
// Führe die SQL-Dateien aus
|
||||
console.log('Führe Supabase-Funktionen-Setup aus...');
|
||||
await executeSqlFile('setup_supabase_functions.sql');
|
||||
|
||||
console.log('Führe Modell-Updates aus...');
|
||||
await executeSqlFile('update_models.sql');
|
||||
|
||||
console.log('Richte RLS-Richtlinien ein...');
|
||||
await executeSqlFile('setup_rls_policies.sql');
|
||||
|
||||
console.log('Supabase-Setup abgeschlossen.');
|
||||
}
|
||||
|
||||
// Führe die Funktion aus
|
||||
setupSupabase()
|
||||
.catch(error => {
|
||||
console.error('Unerwarteter Fehler:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
.catch((error) => {
|
||||
console.error('Unerwarteter Fehler:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,75 +9,75 @@ const path = require('path');
|
|||
const { spawnSync } = require('child_process');
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
|
||||
// Get environment variables
|
||||
// Get environment variables
|
||||
const SUPABASE_URL = process.env.SUPABASE_URL;
|
||||
const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_KEY;
|
||||
|
||||
if (!SUPABASE_URL || !SUPABASE_SERVICE_KEY) {
|
||||
console.error('Error: SUPABASE_URL and SUPABASE_SERVICE_KEY environment variables must be set');
|
||||
process.exit(1);
|
||||
console.error('Error: SUPABASE_URL and SUPABASE_SERVICE_KEY environment variables must be set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create Supabase client
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false
|
||||
}
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
},
|
||||
});
|
||||
|
||||
async function executeSQL(filename) {
|
||||
try {
|
||||
const filePath = path.join(__dirname, filename);
|
||||
const sql = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Split the SQL file by semicolons to get individual statements
|
||||
const statements = sql
|
||||
.split(';')
|
||||
.map(statement => statement.trim())
|
||||
.filter(statement => statement.length > 0);
|
||||
try {
|
||||
const filePath = path.join(__dirname, filename);
|
||||
const sql = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
console.log(`Executing ${statements.length} statements from ${filename}...`);
|
||||
|
||||
for (const statement of statements) {
|
||||
const { error } = await supabase.rpc('exec_sql', { sql: statement });
|
||||
|
||||
if (error) {
|
||||
console.error(`Error executing statement:`, error);
|
||||
console.error(`Statement was: ${statement.substring(0, 100)}...`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Successfully executed ${filename}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`❌ Error executing ${filename}:`, error);
|
||||
return false;
|
||||
}
|
||||
// Split the SQL file by semicolons to get individual statements
|
||||
const statements = sql
|
||||
.split(';')
|
||||
.map((statement) => statement.trim())
|
||||
.filter((statement) => statement.length > 0);
|
||||
|
||||
console.log(`Executing ${statements.length} statements from ${filename}...`);
|
||||
|
||||
for (const statement of statements) {
|
||||
const { error } = await supabase.rpc('exec_sql', { sql: statement });
|
||||
|
||||
if (error) {
|
||||
console.error(`Error executing statement:`, error);
|
||||
console.error(`Statement was: ${statement.substring(0, 100)}...`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Successfully executed ${filename}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`❌ Error executing ${filename}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Setting up spaces feature...');
|
||||
|
||||
// Run the SQL scripts in the correct order
|
||||
const scripts = [
|
||||
'create_spaces_tables.sql',
|
||||
'create_spaces_triggers.sql',
|
||||
'create_spaces_rls.sql'
|
||||
];
|
||||
|
||||
for (const script of scripts) {
|
||||
const success = await executeSQL(script);
|
||||
if (!success) {
|
||||
console.error(`Failed to execute ${script}. Aborting.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Spaces feature setup complete!');
|
||||
console.log('Setting up spaces feature...');
|
||||
|
||||
// Run the SQL scripts in the correct order
|
||||
const scripts = [
|
||||
'create_spaces_tables.sql',
|
||||
'create_spaces_triggers.sql',
|
||||
'create_spaces_rls.sql',
|
||||
];
|
||||
|
||||
for (const script of scripts) {
|
||||
const success = await executeSQL(script);
|
||||
if (!success) {
|
||||
console.error(`Failed to execute ${script}. Aborting.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Spaces feature setup complete!');
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Unhandled error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
main().catch((err) => {
|
||||
console.error('Unhandled error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,148 +17,150 @@ const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
|
|||
const supabaseKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseKey) {
|
||||
console.error('Fehler: EXPO_PUBLIC_SUPABASE_URL und EXPO_PUBLIC_SUPABASE_ANON_KEY müssen in der .env-Datei definiert sein.');
|
||||
process.exit(1);
|
||||
console.error(
|
||||
'Fehler: EXPO_PUBLIC_SUPABASE_URL und EXPO_PUBLIC_SUPABASE_ANON_KEY müssen in der .env-Datei definiert sein.'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||
|
||||
// Readline-Interface für interaktive Eingabe
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
// Funktion zum Ausführen einer SQL-Abfrage
|
||||
async function executeQuery(query) {
|
||||
try {
|
||||
console.log(`Führe Abfrage aus: ${query}`);
|
||||
const { data, error } = await supabase.rpc('execute_sql', { query });
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler bei der Ausführung der Abfrage:', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Ergebnis:');
|
||||
console.table(data);
|
||||
} catch (error) {
|
||||
console.error('Unerwarteter Fehler:', error.message);
|
||||
}
|
||||
try {
|
||||
console.log(`Führe Abfrage aus: ${query}`);
|
||||
const { data, error } = await supabase.rpc('execute_sql', { query });
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler bei der Ausführung der Abfrage:', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Ergebnis:');
|
||||
console.table(data);
|
||||
} catch (error) {
|
||||
console.error('Unerwarteter Fehler:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Funktion zum Ausführen einer SQL-Datei
|
||||
async function executeFile(filePath) {
|
||||
try {
|
||||
const fullPath = join(process.cwd(), filePath);
|
||||
console.log(`Führe SQL-Datei aus: ${fullPath}`);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.error(`Fehler: Datei ${fullPath} existiert nicht.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const query = fs.readFileSync(fullPath, 'utf8');
|
||||
await executeQuery(query);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Lesen oder Ausführen der Datei:', error.message);
|
||||
}
|
||||
try {
|
||||
const fullPath = join(process.cwd(), filePath);
|
||||
console.log(`Führe SQL-Datei aus: ${fullPath}`);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.error(`Fehler: Datei ${fullPath} existiert nicht.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const query = fs.readFileSync(fullPath, 'utf8');
|
||||
await executeQuery(query);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Lesen oder Ausführen der Datei:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Funktion zum Anzeigen der Tabellenliste
|
||||
async function listTables() {
|
||||
try {
|
||||
const query = `
|
||||
try {
|
||||
const query = `
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name;
|
||||
`;
|
||||
|
||||
const { data, error } = await supabase.rpc('execute_sql', { query });
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Abrufen der Tabellenliste:', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Verfügbare Tabellen:');
|
||||
data.forEach((row, index) => {
|
||||
console.log(`${index + 1}. ${row.table_name}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Unerwarteter Fehler:', error.message);
|
||||
}
|
||||
|
||||
const { data, error } = await supabase.rpc('execute_sql', { query });
|
||||
|
||||
if (error) {
|
||||
console.error('Fehler beim Abrufen der Tabellenliste:', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Verfügbare Tabellen:');
|
||||
data.forEach((row, index) => {
|
||||
console.log(`${index + 1}. ${row.table_name}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Unerwarteter Fehler:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Funktion zum Anzeigen der Tabellenstruktur
|
||||
async function describeTable(tableName) {
|
||||
try {
|
||||
const query = `
|
||||
try {
|
||||
const query = `
|
||||
SELECT column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = '${tableName}'
|
||||
ORDER BY ordinal_position;
|
||||
`;
|
||||
|
||||
const { data, error } = await supabase.rpc('execute_sql', { query });
|
||||
|
||||
if (error) {
|
||||
console.error(`Fehler beim Beschreiben der Tabelle ${tableName}:`, error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Struktur der Tabelle ${tableName}:`);
|
||||
console.table(data);
|
||||
} catch (error) {
|
||||
console.error('Unerwarteter Fehler:', error.message);
|
||||
}
|
||||
|
||||
const { data, error } = await supabase.rpc('execute_sql', { query });
|
||||
|
||||
if (error) {
|
||||
console.error(`Fehler beim Beschreiben der Tabelle ${tableName}:`, error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Struktur der Tabelle ${tableName}:`);
|
||||
console.table(data);
|
||||
} catch (error) {
|
||||
console.error('Unerwarteter Fehler:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Hauptmenü
|
||||
function showMenu() {
|
||||
console.log('\n--- Supabase CLI ---');
|
||||
console.log('1. SQL-Abfrage ausführen');
|
||||
console.log('2. SQL-Datei ausführen');
|
||||
console.log('3. Tabellenliste anzeigen');
|
||||
console.log('4. Tabellenstruktur anzeigen');
|
||||
console.log('5. Beenden');
|
||||
|
||||
rl.question('\nWähle eine Option (1-5): ', async (answer) => {
|
||||
switch (answer.trim()) {
|
||||
case '1':
|
||||
rl.question('Gib deine SQL-Abfrage ein: ', async (query) => {
|
||||
await executeQuery(query);
|
||||
showMenu();
|
||||
});
|
||||
break;
|
||||
case '2':
|
||||
rl.question('Gib den Pfad zur SQL-Datei ein: ', async (filePath) => {
|
||||
await executeFile(filePath);
|
||||
showMenu();
|
||||
});
|
||||
break;
|
||||
case '3':
|
||||
await listTables();
|
||||
showMenu();
|
||||
break;
|
||||
case '4':
|
||||
rl.question('Gib den Tabellennamen ein: ', async (tableName) => {
|
||||
await describeTable(tableName);
|
||||
showMenu();
|
||||
});
|
||||
break;
|
||||
case '5':
|
||||
console.log('Auf Wiedersehen!');
|
||||
rl.close();
|
||||
process.exit(0);
|
||||
break;
|
||||
default:
|
||||
console.log('Ungültige Option. Bitte wähle 1-5.');
|
||||
showMenu();
|
||||
break;
|
||||
}
|
||||
});
|
||||
console.log('\n--- Supabase CLI ---');
|
||||
console.log('1. SQL-Abfrage ausführen');
|
||||
console.log('2. SQL-Datei ausführen');
|
||||
console.log('3. Tabellenliste anzeigen');
|
||||
console.log('4. Tabellenstruktur anzeigen');
|
||||
console.log('5. Beenden');
|
||||
|
||||
rl.question('\nWähle eine Option (1-5): ', async (answer) => {
|
||||
switch (answer.trim()) {
|
||||
case '1':
|
||||
rl.question('Gib deine SQL-Abfrage ein: ', async (query) => {
|
||||
await executeQuery(query);
|
||||
showMenu();
|
||||
});
|
||||
break;
|
||||
case '2':
|
||||
rl.question('Gib den Pfad zur SQL-Datei ein: ', async (filePath) => {
|
||||
await executeFile(filePath);
|
||||
showMenu();
|
||||
});
|
||||
break;
|
||||
case '3':
|
||||
await listTables();
|
||||
showMenu();
|
||||
break;
|
||||
case '4':
|
||||
rl.question('Gib den Tabellennamen ein: ', async (tableName) => {
|
||||
await describeTable(tableName);
|
||||
showMenu();
|
||||
});
|
||||
break;
|
||||
case '5':
|
||||
console.log('Auf Wiedersehen!');
|
||||
rl.close();
|
||||
process.exit(0);
|
||||
break;
|
||||
default:
|
||||
console.log('Ungültige Option. Bitte wähle 1-5.');
|
||||
showMenu();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Starte das Programm
|
||||
|
|
|
|||
|
|
@ -15,98 +15,95 @@ const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
|
|||
const supabaseKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseKey) {
|
||||
console.error('Fehler: EXPO_PUBLIC_SUPABASE_URL und EXPO_PUBLIC_SUPABASE_ANON_KEY müssen in der .env-Datei definiert sein.');
|
||||
process.exit(1);
|
||||
console.error(
|
||||
'Fehler: EXPO_PUBLIC_SUPABASE_URL und EXPO_PUBLIC_SUPABASE_ANON_KEY müssen in der .env-Datei definiert sein.'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||
|
||||
// Modelle, die wir in die Datenbank einfügen wollen
|
||||
const models = [
|
||||
{
|
||||
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'
|
||||
}
|
||||
}
|
||||
{
|
||||
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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
async function updateModels() {
|
||||
console.log('Aktualisiere Modelle in der Supabase-Datenbank...');
|
||||
|
||||
// Prüfe, ob die Tabelle existiert
|
||||
const { error: tableError } = await supabase
|
||||
.from('models')
|
||||
.select('id')
|
||||
.limit(1);
|
||||
|
||||
if (tableError) {
|
||||
console.error('Fehler beim Zugriff auf die models-Tabelle:', tableError.message);
|
||||
console.log('Erstelle models-Tabelle...');
|
||||
|
||||
// Erstelle die Tabelle, falls sie nicht existiert
|
||||
const { error: createError } = await supabase.rpc('create_models_table');
|
||||
|
||||
if (createError) {
|
||||
console.error('Fehler beim Erstellen der models-Tabelle:', createError.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Füge die Modelle ein oder aktualisiere sie
|
||||
for (const model of models) {
|
||||
const { error } = await supabase
|
||||
.from('models')
|
||||
.upsert(model, { onConflict: 'id' });
|
||||
|
||||
if (error) {
|
||||
console.error(`Fehler beim Aktualisieren des Modells ${model.name}:`, error.message);
|
||||
} else {
|
||||
console.log(`Modell ${model.name} erfolgreich aktualisiert.`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Modellaktualisierung abgeschlossen.');
|
||||
console.log('Aktualisiere Modelle in der Supabase-Datenbank...');
|
||||
|
||||
// Prüfe, ob die Tabelle existiert
|
||||
const { error: tableError } = await supabase.from('models').select('id').limit(1);
|
||||
|
||||
if (tableError) {
|
||||
console.error('Fehler beim Zugriff auf die models-Tabelle:', tableError.message);
|
||||
console.log('Erstelle models-Tabelle...');
|
||||
|
||||
// Erstelle die Tabelle, falls sie nicht existiert
|
||||
const { error: createError } = await supabase.rpc('create_models_table');
|
||||
|
||||
if (createError) {
|
||||
console.error('Fehler beim Erstellen der models-Tabelle:', createError.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Füge die Modelle ein oder aktualisiere sie
|
||||
for (const model of models) {
|
||||
const { error } = await supabase.from('models').upsert(model, { onConflict: 'id' });
|
||||
|
||||
if (error) {
|
||||
console.error(`Fehler beim Aktualisieren des Modells ${model.name}:`, error.message);
|
||||
} else {
|
||||
console.log(`Modell ${model.name} erfolgreich aktualisiert.`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Modellaktualisierung abgeschlossen.');
|
||||
}
|
||||
|
||||
// Führe die Funktion aus
|
||||
updateModels()
|
||||
.catch(error => {
|
||||
console.error('Unerwarteter Fehler:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
.catch((error) => {
|
||||
console.error('Unerwarteter Fehler:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,300 +2,300 @@
|
|||
* Conversation Service - CRUD operations via Backend API
|
||||
*/
|
||||
import {
|
||||
conversationApi,
|
||||
chatApi,
|
||||
modelApi,
|
||||
templateApi,
|
||||
usageApi,
|
||||
type Conversation as ApiConversation,
|
||||
type Message as ApiMessage,
|
||||
type ChatMessage,
|
||||
type TokenUsage,
|
||||
conversationApi,
|
||||
chatApi,
|
||||
modelApi,
|
||||
templateApi,
|
||||
usageApi,
|
||||
type Conversation as ApiConversation,
|
||||
type Message as ApiMessage,
|
||||
type ChatMessage,
|
||||
type TokenUsage,
|
||||
} from './api';
|
||||
|
||||
// Re-export types with backwards-compatible naming (snake_case for mobile)
|
||||
export type Conversation = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
model_id: string;
|
||||
template_id?: string;
|
||||
space_id?: string;
|
||||
conversation_mode: 'free' | 'guided' | 'template';
|
||||
document_mode: boolean;
|
||||
title?: string;
|
||||
is_archived: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
id: string;
|
||||
user_id: string;
|
||||
model_id: string;
|
||||
template_id?: string;
|
||||
space_id?: string;
|
||||
conversation_mode: 'free' | 'guided' | 'template';
|
||||
document_mode: boolean;
|
||||
title?: string;
|
||||
is_archived: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type Message = {
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
sender: 'user' | 'assistant' | 'system';
|
||||
message_text: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
sender: 'user' | 'assistant' | 'system';
|
||||
message_text: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type TokenUsageType = {
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
message_id: string;
|
||||
user_id: string;
|
||||
model_id: string;
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
estimated_cost: number;
|
||||
created_at: string;
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
message_id: string;
|
||||
user_id: string;
|
||||
model_id: string;
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
estimated_cost: number;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
// Helper to convert API response to local format
|
||||
function toLocalConversation(conv: ApiConversation): Conversation {
|
||||
return {
|
||||
id: conv.id,
|
||||
user_id: conv.userId,
|
||||
model_id: conv.modelId,
|
||||
template_id: conv.templateId,
|
||||
space_id: conv.spaceId,
|
||||
conversation_mode: conv.conversationMode,
|
||||
document_mode: conv.documentMode,
|
||||
title: conv.title,
|
||||
is_archived: conv.isArchived,
|
||||
created_at: conv.createdAt,
|
||||
updated_at: conv.updatedAt,
|
||||
};
|
||||
return {
|
||||
id: conv.id,
|
||||
user_id: conv.userId,
|
||||
model_id: conv.modelId,
|
||||
template_id: conv.templateId,
|
||||
space_id: conv.spaceId,
|
||||
conversation_mode: conv.conversationMode,
|
||||
document_mode: conv.documentMode,
|
||||
title: conv.title,
|
||||
is_archived: conv.isArchived,
|
||||
created_at: conv.createdAt,
|
||||
updated_at: conv.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function toLocalMessage(msg: ApiMessage): Message {
|
||||
return {
|
||||
id: msg.id,
|
||||
conversation_id: msg.conversationId,
|
||||
sender: msg.sender,
|
||||
message_text: msg.messageText,
|
||||
created_at: msg.createdAt,
|
||||
updated_at: msg.updatedAt,
|
||||
};
|
||||
return {
|
||||
id: msg.id,
|
||||
conversation_id: msg.conversationId,
|
||||
sender: msg.sender,
|
||||
message_text: msg.messageText,
|
||||
created_at: msg.createdAt,
|
||||
updated_at: msg.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine neue Konversation in der Datenbank
|
||||
*/
|
||||
export async function createConversation(
|
||||
userId: string,
|
||||
modelId: string,
|
||||
mode: 'free' | 'guided' | 'template' = 'free',
|
||||
templateId?: string,
|
||||
documentMode: boolean = false,
|
||||
spaceId?: string
|
||||
userId: string,
|
||||
modelId: string,
|
||||
mode: 'free' | 'guided' | 'template' = 'free',
|
||||
templateId?: string,
|
||||
documentMode: boolean = false,
|
||||
spaceId?: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
console.log('🔵 Erstelle Konversation mit Space ID:', spaceId || 'keine');
|
||||
try {
|
||||
console.log('🔵 Erstelle Konversation mit Space ID:', spaceId || 'keine');
|
||||
|
||||
const conversation = await conversationApi.createConversation({
|
||||
modelId,
|
||||
conversationMode: mode,
|
||||
templateId,
|
||||
documentMode,
|
||||
spaceId,
|
||||
});
|
||||
const conversation = await conversationApi.createConversation({
|
||||
modelId,
|
||||
conversationMode: mode,
|
||||
templateId,
|
||||
documentMode,
|
||||
spaceId,
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
console.error('Fehler beim Erstellen der Konversation');
|
||||
return null;
|
||||
}
|
||||
if (!conversation) {
|
||||
console.error('Fehler beim Erstellen der Konversation');
|
||||
return null;
|
||||
}
|
||||
|
||||
return conversation.id;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Konversation:', error);
|
||||
return null;
|
||||
}
|
||||
return conversation.id;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Konversation:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt eine neue Nachricht zur Konversation hinzu
|
||||
*/
|
||||
export async function addMessage(
|
||||
conversationId: string,
|
||||
sender: 'user' | 'assistant' | 'system',
|
||||
messageText: string
|
||||
conversationId: string,
|
||||
sender: 'user' | 'assistant' | 'system',
|
||||
messageText: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
// Validate sender
|
||||
let validSender = sender;
|
||||
if (!['user', 'assistant', 'system'].includes(validSender)) {
|
||||
console.error('Ungültiger Sender-Wert:', sender);
|
||||
validSender = 'user';
|
||||
}
|
||||
try {
|
||||
// Validate sender
|
||||
let validSender = sender;
|
||||
if (!['user', 'assistant', 'system'].includes(validSender)) {
|
||||
console.error('Ungültiger Sender-Wert:', sender);
|
||||
validSender = 'user';
|
||||
}
|
||||
|
||||
console.log('Füge Nachricht hinzu mit Sender:', validSender);
|
||||
console.log('Füge Nachricht hinzu mit Sender:', validSender);
|
||||
|
||||
const message = await conversationApi.addMessage(conversationId, validSender, messageText);
|
||||
const message = await conversationApi.addMessage(conversationId, validSender, messageText);
|
||||
|
||||
if (!message) {
|
||||
console.error('Fehler beim Hinzufügen der Nachricht');
|
||||
return null;
|
||||
}
|
||||
if (!message) {
|
||||
console.error('Fehler beim Hinzufügen der Nachricht');
|
||||
return null;
|
||||
}
|
||||
|
||||
return message.id;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Hinzufügen der Nachricht:', error);
|
||||
return null;
|
||||
}
|
||||
return message.id;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Hinzufügen der Nachricht:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle Nachrichten einer Konversation
|
||||
*/
|
||||
export async function getMessages(conversationId: string): Promise<Message[]> {
|
||||
try {
|
||||
const messages = await conversationApi.getMessages(conversationId);
|
||||
return messages.map(toLocalMessage);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Nachrichten:', error);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const messages = await conversationApi.getMessages(conversationId);
|
||||
return messages.map(toLocalMessage);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Nachrichten:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen Titel für die Konversation basierend auf der ersten Benutzeranfrage
|
||||
*/
|
||||
export async function generateConversationTitle(userQuestion: string): Promise<string> {
|
||||
try {
|
||||
console.log(
|
||||
'Generiere Titel für Konversation basierend auf:',
|
||||
userQuestion.substring(0, 50) + (userQuestion.length > 50 ? '...' : '')
|
||||
);
|
||||
try {
|
||||
console.log(
|
||||
'Generiere Titel für Konversation basierend auf:',
|
||||
userQuestion.substring(0, 50) + (userQuestion.length > 50 ? '...' : '')
|
||||
);
|
||||
|
||||
const titlePrompt = `Schreibe eine kurze, prägnante Überschrift (maximal 5 Wörter) für unseren Chat mit dieser Frage: "${userQuestion}"`;
|
||||
const titlePrompt = `Schreibe eine kurze, prägnante Überschrift (maximal 5 Wörter) für unseren Chat mit dieser Frage: "${userQuestion}"`;
|
||||
|
||||
const response = await chatApi.createCompletion({
|
||||
messages: [{ role: 'user', content: titlePrompt }],
|
||||
modelId: '550e8400-e29b-41d4-a716-446655440004', // GPT-4o-Mini
|
||||
temperature: 0.3,
|
||||
maxTokens: 50,
|
||||
});
|
||||
const response = await chatApi.createCompletion({
|
||||
messages: [{ role: 'user', content: titlePrompt }],
|
||||
modelId: '550e8400-e29b-41d4-a716-446655440004', // GPT-4o-Mini
|
||||
temperature: 0.3,
|
||||
maxTokens: 50,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
return 'Neue Konversation';
|
||||
}
|
||||
if (!response) {
|
||||
return 'Neue Konversation';
|
||||
}
|
||||
|
||||
// Clean up title
|
||||
let cleanTitle = response.content
|
||||
.trim()
|
||||
.replace(/^["']|["']$/g, '')
|
||||
.replace(/\.$/g, '');
|
||||
// Clean up title
|
||||
let cleanTitle = response.content
|
||||
.trim()
|
||||
.replace(/^["']|["']$/g, '')
|
||||
.replace(/\.$/g, '');
|
||||
|
||||
if (cleanTitle.length > 100) {
|
||||
cleanTitle = cleanTitle.substring(0, 97) + '...';
|
||||
}
|
||||
if (cleanTitle.length > 100) {
|
||||
cleanTitle = cleanTitle.substring(0, 97) + '...';
|
||||
}
|
||||
|
||||
console.log('Generierter Titel:', cleanTitle);
|
||||
return cleanTitle;
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Titelgenerierung:', error);
|
||||
return 'Neue Konversation';
|
||||
}
|
||||
console.log('Generierter Titel:', cleanTitle);
|
||||
return cleanTitle;
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Titelgenerierung:', error);
|
||||
return 'Neue Konversation';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert den Titel einer Konversation
|
||||
*/
|
||||
export async function updateConversationTitle(
|
||||
conversationId: string,
|
||||
title: string
|
||||
conversationId: string,
|
||||
title: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const success = await conversationApi.updateTitle(conversationId, title);
|
||||
try {
|
||||
const success = await conversationApi.updateTitle(conversationId, title);
|
||||
|
||||
if (!success) {
|
||||
console.error('Fehler beim Aktualisieren des Konversationstitels');
|
||||
return false;
|
||||
}
|
||||
if (!success) {
|
||||
console.error('Fehler beim Aktualisieren des Konversationstitels');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Konversationstitels:', error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Konversationstitels:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt einen System-Prompt aus einer Vorlage
|
||||
*/
|
||||
export async function getSystemPromptFromTemplate(templateId: string): Promise<string | null> {
|
||||
try {
|
||||
const template = await templateApi.getTemplate(templateId);
|
||||
try {
|
||||
const template = await templateApi.getTemplate(templateId);
|
||||
|
||||
if (!template) {
|
||||
console.error('Fehler beim Laden der Vorlage');
|
||||
return null;
|
||||
}
|
||||
if (!template) {
|
||||
console.error('Fehler beim Laden der Vorlage');
|
||||
return null;
|
||||
}
|
||||
|
||||
return template.systemPrompt;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Vorlage:', error);
|
||||
return null;
|
||||
}
|
||||
return template.systemPrompt;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Vorlage:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet eine Benutzeranfrage an das LLM-Modell und speichert die Antwort
|
||||
*/
|
||||
export async function sendMessageAndGetResponse(
|
||||
conversationId: string,
|
||||
userMessage: string,
|
||||
modelId: string,
|
||||
templateId?: string,
|
||||
documentMode: boolean = false
|
||||
conversationId: string,
|
||||
userMessage: string,
|
||||
modelId: string,
|
||||
templateId?: string,
|
||||
documentMode: boolean = false
|
||||
): Promise<{
|
||||
userMessageId: string | null;
|
||||
assistantMessageId: string | null;
|
||||
assistantResponse: string;
|
||||
title?: string;
|
||||
documentContent?: string;
|
||||
userMessageId: string | null;
|
||||
assistantMessageId: string | null;
|
||||
assistantResponse: string;
|
||||
title?: string;
|
||||
documentContent?: string;
|
||||
}> {
|
||||
try {
|
||||
console.log('Starte sendMessageAndGetResponse mit:', {
|
||||
conversationId,
|
||||
userMessage: userMessage.substring(0, 50) + (userMessage.length > 50 ? '...' : ''),
|
||||
modelId,
|
||||
});
|
||||
try {
|
||||
console.log('Starte sendMessageAndGetResponse mit:', {
|
||||
conversationId,
|
||||
userMessage: userMessage.substring(0, 50) + (userMessage.length > 50 ? '...' : ''),
|
||||
modelId,
|
||||
});
|
||||
|
||||
// Load the model from API
|
||||
const modelData = await modelApi.getModel(modelId);
|
||||
// Load the model from API
|
||||
const modelData = await modelApi.getModel(modelId);
|
||||
|
||||
if (!modelData) {
|
||||
console.log('Verwende Standard-Parameter, da Modell nicht geladen werden konnte');
|
||||
} else {
|
||||
console.log('Modell geladen:', {
|
||||
id: modelData.id,
|
||||
name: modelData.name,
|
||||
parameters: modelData.parameters,
|
||||
deployment: modelData.parameters?.deployment,
|
||||
});
|
||||
}
|
||||
if (!modelData) {
|
||||
console.log('Verwende Standard-Parameter, da Modell nicht geladen werden konnte');
|
||||
} else {
|
||||
console.log('Modell geladen:', {
|
||||
id: modelData.id,
|
||||
name: modelData.name,
|
||||
parameters: modelData.parameters,
|
||||
deployment: modelData.parameters?.deployment,
|
||||
});
|
||||
}
|
||||
|
||||
// Save the user message
|
||||
const userMessageId = await addMessage(conversationId, 'user', userMessage);
|
||||
console.log('Benutzernachricht gespeichert mit ID:', userMessageId);
|
||||
// Save the user message
|
||||
const userMessageId = await addMessage(conversationId, 'user', userMessage);
|
||||
console.log('Benutzernachricht gespeichert mit ID:', userMessageId);
|
||||
|
||||
// Load all messages for context
|
||||
const messages = await getMessages(conversationId);
|
||||
console.log(`${messages.length} Nachrichten für Kontext geladen`);
|
||||
// Load all messages for context
|
||||
const messages = await getMessages(conversationId);
|
||||
console.log(`${messages.length} Nachrichten für Kontext geladen`);
|
||||
|
||||
// Build chat messages for API
|
||||
const chatMessages: ChatMessage[] = [];
|
||||
// Build chat messages for API
|
||||
const chatMessages: ChatMessage[] = [];
|
||||
|
||||
// Load system prompt from template if available
|
||||
let systemPrompt: string | null = null;
|
||||
if (templateId) {
|
||||
systemPrompt = await getSystemPromptFromTemplate(templateId);
|
||||
if (systemPrompt) {
|
||||
console.log('System-Prompt aus Vorlage geladen');
|
||||
// Load system prompt from template if available
|
||||
let systemPrompt: string | null = null;
|
||||
if (templateId) {
|
||||
systemPrompt = await getSystemPromptFromTemplate(templateId);
|
||||
if (systemPrompt) {
|
||||
console.log('System-Prompt aus Vorlage geladen');
|
||||
|
||||
if (documentMode) {
|
||||
const documentModePrompt = `
|
||||
if (documentMode) {
|
||||
const documentModePrompt = `
|
||||
${systemPrompt}
|
||||
|
||||
WICHTIG: Du befindest dich im Dokumentmodus. Deine Aufgabe ist es, dem Benutzer zu helfen, ein Dokument zu erstellen und zu verbessern.
|
||||
|
|
@ -311,14 +311,14 @@ CHAT: Hier antwortest du auf die Frage oder das Feedback des Nutzers.
|
|||
DOKUMENT:
|
||||
Hier steht dein Vorschlag für das Dokument in Markdown-Format, ohne Codeblock-Markierungen.
|
||||
`;
|
||||
chatMessages.push({ role: 'system', content: documentModePrompt });
|
||||
console.log('Dokumentmodus-Prompt hinzugefügt');
|
||||
} else {
|
||||
chatMessages.push({ role: 'system', content: systemPrompt });
|
||||
}
|
||||
}
|
||||
} else if (documentMode) {
|
||||
const documentModePrompt = `
|
||||
chatMessages.push({ role: 'system', content: documentModePrompt });
|
||||
console.log('Dokumentmodus-Prompt hinzugefügt');
|
||||
} else {
|
||||
chatMessages.push({ role: 'system', content: systemPrompt });
|
||||
}
|
||||
}
|
||||
} else if (documentMode) {
|
||||
const documentModePrompt = `
|
||||
Du befindest dich im Dokumentmodus. Deine Aufgabe ist es, dem Benutzer zu helfen, ein Dokument zu erstellen und zu verbessern.
|
||||
|
||||
1. Das Dokument wird in einem separaten Bereich neben dem Chat angezeigt.
|
||||
|
|
@ -332,219 +332,219 @@ CHAT: Hier antwortest du auf die Frage oder das Feedback des Nutzers.
|
|||
DOKUMENT:
|
||||
Hier steht dein Vorschlag für das Dokument in Markdown-Format, ohne Codeblock-Markierungen.
|
||||
`;
|
||||
chatMessages.push({ role: 'system', content: documentModePrompt });
|
||||
console.log('Standard-Dokumentmodus-Prompt hinzugefügt');
|
||||
}
|
||||
chatMessages.push({ role: 'system', content: documentModePrompt });
|
||||
console.log('Standard-Dokumentmodus-Prompt hinzugefügt');
|
||||
}
|
||||
|
||||
// Add all conversation messages
|
||||
chatMessages.push(
|
||||
...messages.map((msg) => ({
|
||||
role: msg.sender === 'user' ? 'user' : msg.sender === 'assistant' ? 'assistant' : 'system',
|
||||
content: msg.message_text,
|
||||
})) as ChatMessage[]
|
||||
);
|
||||
// Add all conversation messages
|
||||
chatMessages.push(
|
||||
...(messages.map((msg) => ({
|
||||
role: msg.sender === 'user' ? 'user' : msg.sender === 'assistant' ? 'assistant' : 'system',
|
||||
content: msg.message_text,
|
||||
})) as ChatMessage[])
|
||||
);
|
||||
|
||||
console.log('Nachrichten für OpenAI konvertiert:', chatMessages.length, 'Nachrichten');
|
||||
console.log('Nachrichten für OpenAI konvertiert:', chatMessages.length, 'Nachrichten');
|
||||
|
||||
// Send request to AI
|
||||
console.log('Sende Anfrage an LLM-Modell mit Parametern:', {
|
||||
temperature: modelData?.parameters?.temperature || 0.7,
|
||||
maxTokens: modelData?.parameters?.max_tokens || 800,
|
||||
});
|
||||
// Send request to AI
|
||||
console.log('Sende Anfrage an LLM-Modell mit Parametern:', {
|
||||
temperature: modelData?.parameters?.temperature || 0.7,
|
||||
maxTokens: modelData?.parameters?.max_tokens || 800,
|
||||
});
|
||||
|
||||
const result = await chatApi.createCompletion({
|
||||
messages: chatMessages,
|
||||
modelId,
|
||||
temperature: modelData?.parameters?.temperature || 0.7,
|
||||
maxTokens: modelData?.parameters?.max_tokens || 800,
|
||||
});
|
||||
const result = await chatApi.createCompletion({
|
||||
messages: chatMessages,
|
||||
modelId,
|
||||
temperature: modelData?.parameters?.temperature || 0.7,
|
||||
maxTokens: modelData?.parameters?.max_tokens || 800,
|
||||
});
|
||||
|
||||
let assistantResponse: string;
|
||||
let tokenUsage: TokenUsage | undefined;
|
||||
let assistantResponse: string;
|
||||
let tokenUsage: TokenUsage | undefined;
|
||||
|
||||
if (!result) {
|
||||
assistantResponse =
|
||||
'Es konnte keine Antwort generiert werden. Bitte stelle sicher, dass das Backend läuft.';
|
||||
} else {
|
||||
assistantResponse = result.content;
|
||||
tokenUsage = result.usage;
|
||||
if (!result) {
|
||||
assistantResponse =
|
||||
'Es konnte keine Antwort generiert werden. Bitte stelle sicher, dass das Backend läuft.';
|
||||
} else {
|
||||
assistantResponse = result.content;
|
||||
tokenUsage = result.usage;
|
||||
|
||||
console.log('Antwort vom LLM-Modell erhalten:', {
|
||||
length: assistantResponse.length,
|
||||
preview: assistantResponse.substring(0, 50) + (assistantResponse.length > 50 ? '...' : ''),
|
||||
tokenUsage,
|
||||
});
|
||||
}
|
||||
console.log('Antwort vom LLM-Modell erhalten:', {
|
||||
length: assistantResponse.length,
|
||||
preview: assistantResponse.substring(0, 50) + (assistantResponse.length > 50 ? '...' : ''),
|
||||
tokenUsage,
|
||||
});
|
||||
}
|
||||
|
||||
// Extract document content if in document mode
|
||||
let documentContent: string | undefined;
|
||||
let chatResponse = assistantResponse;
|
||||
// Extract document content if in document mode
|
||||
let documentContent: string | undefined;
|
||||
let chatResponse = assistantResponse;
|
||||
|
||||
if (documentMode) {
|
||||
console.log(
|
||||
'Analysiere LLM-Antwort für Dokumentextraktion:',
|
||||
assistantResponse.substring(0, 200) + '...'
|
||||
);
|
||||
if (documentMode) {
|
||||
console.log(
|
||||
'Analysiere LLM-Antwort für Dokumentextraktion:',
|
||||
assistantResponse.substring(0, 200) + '...'
|
||||
);
|
||||
|
||||
const chatMatch = assistantResponse.match(/CHAT:(.*?)(?=DOKUMENT:|$)/s);
|
||||
const documentMatch = assistantResponse.match(
|
||||
/DOKUMENT:[\s\n]*(```markdown|```|`markdown)?([^`].*?)(?:```|`+)?$/s
|
||||
);
|
||||
const chatMatch = assistantResponse.match(/CHAT:(.*?)(?=DOKUMENT:|$)/s);
|
||||
const documentMatch = assistantResponse.match(
|
||||
/DOKUMENT:[\s\n]*(```markdown|```|`markdown)?([^`].*?)(?:```|`+)?$/s
|
||||
);
|
||||
|
||||
console.log('Dokument-Regex Match:', documentMatch ? 'Ja' : 'Nein');
|
||||
console.log('Dokument-Regex Match:', documentMatch ? 'Ja' : 'Nein');
|
||||
|
||||
if (chatMatch && chatMatch[1]) {
|
||||
chatResponse = chatMatch[1].trim();
|
||||
console.log('Chat-Antwort extrahiert:', chatResponse.substring(0, 50) + '...');
|
||||
}
|
||||
if (chatMatch && chatMatch[1]) {
|
||||
chatResponse = chatMatch[1].trim();
|
||||
console.log('Chat-Antwort extrahiert:', chatResponse.substring(0, 50) + '...');
|
||||
}
|
||||
|
||||
if (documentMatch && documentMatch[2]) {
|
||||
documentContent = documentMatch[2].trim();
|
||||
console.log(
|
||||
'Dokument-Inhalt extrahiert:',
|
||||
(documentContent?.substring(0, 50) || '') + '...'
|
||||
);
|
||||
}
|
||||
}
|
||||
if (documentMatch && documentMatch[2]) {
|
||||
documentContent = documentMatch[2].trim();
|
||||
console.log(
|
||||
'Dokument-Inhalt extrahiert:',
|
||||
(documentContent?.substring(0, 50) || '') + '...'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Save assistant message
|
||||
const assistantMessageId = await addMessage(conversationId, 'assistant', chatResponse);
|
||||
console.log('Assistentenantwort gespeichert mit ID:', assistantMessageId);
|
||||
// Save assistant message
|
||||
const assistantMessageId = await addMessage(conversationId, 'assistant', chatResponse);
|
||||
console.log('Assistentenantwort gespeichert mit ID:', assistantMessageId);
|
||||
|
||||
// Log token usage if available
|
||||
if (tokenUsage && assistantMessageId && userMessageId) {
|
||||
try {
|
||||
const estimatedCost = calculateTokenCost(
|
||||
tokenUsage.prompt_tokens,
|
||||
tokenUsage.completion_tokens,
|
||||
modelData?.costSettings
|
||||
);
|
||||
// Log token usage if available
|
||||
if (tokenUsage && assistantMessageId && userMessageId) {
|
||||
try {
|
||||
const estimatedCost = calculateTokenCost(
|
||||
tokenUsage.prompt_tokens,
|
||||
tokenUsage.completion_tokens,
|
||||
modelData?.costSettings
|
||||
);
|
||||
|
||||
await usageApi.logTokenUsage({
|
||||
conversationId,
|
||||
messageId: assistantMessageId,
|
||||
modelId,
|
||||
promptTokens: tokenUsage.prompt_tokens,
|
||||
completionTokens: tokenUsage.completion_tokens,
|
||||
totalTokens: tokenUsage.total_tokens,
|
||||
estimatedCost,
|
||||
});
|
||||
console.log('Token-Nutzung erfolgreich geloggt');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Loggen der Token-Nutzung:', error);
|
||||
}
|
||||
}
|
||||
await usageApi.logTokenUsage({
|
||||
conversationId,
|
||||
messageId: assistantMessageId,
|
||||
modelId,
|
||||
promptTokens: tokenUsage.prompt_tokens,
|
||||
completionTokens: tokenUsage.completion_tokens,
|
||||
totalTokens: tokenUsage.total_tokens,
|
||||
estimatedCost,
|
||||
});
|
||||
console.log('Token-Nutzung erfolgreich geloggt');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Loggen der Token-Nutzung:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate title for new conversations
|
||||
const allMessages = await getMessages(conversationId);
|
||||
let title: string | undefined;
|
||||
// Generate title for new conversations
|
||||
const allMessages = await getMessages(conversationId);
|
||||
let title: string | undefined;
|
||||
|
||||
if (allMessages.length <= 2) {
|
||||
title = await generateConversationTitle(userMessage);
|
||||
if (allMessages.length <= 2) {
|
||||
title = await generateConversationTitle(userMessage);
|
||||
|
||||
if (title) {
|
||||
const success = await updateConversationTitle(conversationId, title);
|
||||
console.log('Konversationstitel aktualisiert:', success ? 'erfolgreich' : 'fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
if (title) {
|
||||
const success = await updateConversationTitle(conversationId, title);
|
||||
console.log('Konversationstitel aktualisiert:', success ? 'erfolgreich' : 'fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userMessageId,
|
||||
assistantMessageId,
|
||||
assistantResponse: chatResponse,
|
||||
title,
|
||||
documentContent,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Senden der Nachricht:', error);
|
||||
return {
|
||||
userMessageId,
|
||||
assistantMessageId,
|
||||
assistantResponse: chatResponse,
|
||||
title,
|
||||
documentContent,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Senden der Nachricht:', error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
console.error('Fehlerdetails:', {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
console.error('Fehlerdetails:', {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
userMessageId: null,
|
||||
assistantMessageId: null,
|
||||
assistantResponse: `Es ist ein Fehler aufgetreten: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}. Bitte versuche es später erneut.`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
userMessageId: null,
|
||||
assistantMessageId: null,
|
||||
assistantResponse: `Es ist ein Fehler aufgetreten: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}. Bitte versuche es später erneut.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to calculate token cost
|
||||
function calculateTokenCost(
|
||||
promptTokens: number,
|
||||
completionTokens: number,
|
||||
costSettings?: { prompt_per_1k_tokens?: number; completion_per_1k_tokens?: number }
|
||||
promptTokens: number,
|
||||
completionTokens: number,
|
||||
costSettings?: { prompt_per_1k_tokens?: number; completion_per_1k_tokens?: number }
|
||||
): number {
|
||||
const promptCost = costSettings?.prompt_per_1k_tokens || 0.0001;
|
||||
const completionCost = costSettings?.completion_per_1k_tokens || 0.0002;
|
||||
const promptCost = costSettings?.prompt_per_1k_tokens || 0.0001;
|
||||
const completionCost = costSettings?.completion_per_1k_tokens || 0.0002;
|
||||
|
||||
const cost = (promptTokens * promptCost + completionTokens * completionCost) / 1000;
|
||||
return Number(cost.toFixed(6));
|
||||
const cost = (promptTokens * promptCost + completionTokens * completionCost) / 1000;
|
||||
return Number(cost.toFixed(6));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle aktiven (nicht archivierten) Konversationen eines Benutzers
|
||||
*/
|
||||
export async function getConversations(userId: string, spaceId?: string): Promise<Conversation[]> {
|
||||
try {
|
||||
const conversations = await conversationApi.getConversations(spaceId);
|
||||
return conversations.map(toLocalConversation);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Konversationen:', error);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const conversations = await conversationApi.getConversations(spaceId);
|
||||
return conversations.map(toLocalConversation);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Konversationen:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle archivierten Konversationen eines Benutzers
|
||||
*/
|
||||
export async function getArchivedConversations(userId: string): Promise<Conversation[]> {
|
||||
try {
|
||||
const conversations = await conversationApi.getArchivedConversations();
|
||||
return conversations.map(toLocalConversation);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der archivierten Konversationen:', error);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const conversations = await conversationApi.getArchivedConversations();
|
||||
return conversations.map(toLocalConversation);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der archivierten Konversationen:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Archiviert eine Konversation
|
||||
*/
|
||||
export async function archiveConversation(conversationId: string): Promise<boolean> {
|
||||
try {
|
||||
return await conversationApi.archiveConversation(conversationId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Archivieren der Konversation:', error);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await conversationApi.archiveConversation(conversationId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Archivieren der Konversation:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stellt eine archivierte Konversation wieder her
|
||||
*/
|
||||
export async function unarchiveConversation(conversationId: string): Promise<boolean> {
|
||||
try {
|
||||
return await conversationApi.unarchiveConversation(conversationId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Wiederherstellen der Konversation:', error);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await conversationApi.unarchiveConversation(conversationId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Wiederherstellen der Konversation:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht eine Konversation dauerhaft
|
||||
*/
|
||||
export async function deleteConversation(conversationId: string): Promise<boolean> {
|
||||
try {
|
||||
return await conversationApi.deleteConversation(conversationId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Konversation:', error);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await conversationApi.deleteConversation(conversationId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Konversation:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,157 +5,157 @@ import { documentApi, type Document as ApiDocument } from './api';
|
|||
|
||||
// Re-export type with backwards-compatible naming (snake_case for mobile)
|
||||
export interface Document {
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
version: number;
|
||||
content: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
version: number;
|
||||
content: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// Helper to convert API response to local format
|
||||
function toLocalDocument(doc: ApiDocument): Document {
|
||||
return {
|
||||
id: doc.id,
|
||||
conversation_id: doc.conversationId,
|
||||
version: doc.version,
|
||||
content: doc.content,
|
||||
created_at: doc.createdAt,
|
||||
updated_at: doc.updatedAt,
|
||||
};
|
||||
return {
|
||||
id: doc.id,
|
||||
conversation_id: doc.conversationId,
|
||||
version: doc.version,
|
||||
content: doc.content,
|
||||
created_at: doc.createdAt,
|
||||
updated_at: doc.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt ein neues Dokument in einer Konversation
|
||||
*/
|
||||
export async function createDocument(
|
||||
conversationId: string,
|
||||
content: string
|
||||
conversationId: string,
|
||||
content: string
|
||||
): Promise<Document | null> {
|
||||
try {
|
||||
console.log(
|
||||
`Erstelle Dokument für Konversation ${conversationId} mit Inhalt: ${content.substring(0, 50)}...`
|
||||
);
|
||||
try {
|
||||
console.log(
|
||||
`Erstelle Dokument für Konversation ${conversationId} mit Inhalt: ${content.substring(0, 50)}...`
|
||||
);
|
||||
|
||||
const document = await documentApi.createDocument(conversationId, content);
|
||||
const document = await documentApi.createDocument(conversationId, content);
|
||||
|
||||
if (!document) {
|
||||
console.error('Fehler beim Erstellen des Dokuments');
|
||||
return null;
|
||||
}
|
||||
if (!document) {
|
||||
console.error('Fehler beim Erstellen des Dokuments');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('Dokument erfolgreich erstellt:', document);
|
||||
return toLocalDocument(document);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Dokuments:', error);
|
||||
if (error instanceof Error) {
|
||||
console.error('Error details:', error.message, error.stack);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
console.log('Dokument erfolgreich erstellt:', document);
|
||||
return toLocalDocument(document);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Dokuments:', error);
|
||||
if (error instanceof Error) {
|
||||
console.error('Error details:', error.message, error.stack);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine neue Version eines Dokuments
|
||||
*/
|
||||
export async function createDocumentVersion(
|
||||
conversationId: string,
|
||||
content: string
|
||||
conversationId: string,
|
||||
content: string
|
||||
): Promise<Document | null> {
|
||||
try {
|
||||
const document = await documentApi.createDocumentVersion(conversationId, content);
|
||||
try {
|
||||
const document = await documentApi.createDocumentVersion(conversationId, content);
|
||||
|
||||
if (!document) {
|
||||
console.error('Fehler beim Erstellen der neuen Dokumentversion');
|
||||
return null;
|
||||
}
|
||||
if (!document) {
|
||||
console.error('Fehler beim Erstellen der neuen Dokumentversion');
|
||||
return null;
|
||||
}
|
||||
|
||||
return toLocalDocument(document);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der neuen Dokumentversion:', error);
|
||||
return null;
|
||||
}
|
||||
return toLocalDocument(document);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der neuen Dokumentversion:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt die aktuellste Version eines Dokuments für eine Konversation
|
||||
*/
|
||||
export async function getLatestDocument(conversationId: string): Promise<Document | null> {
|
||||
try {
|
||||
console.log(`Lade neuestes Dokument für Konversation ${conversationId}`);
|
||||
try {
|
||||
console.log(`Lade neuestes Dokument für Konversation ${conversationId}`);
|
||||
|
||||
const document = await documentApi.getLatestDocument(conversationId);
|
||||
const document = await documentApi.getLatestDocument(conversationId);
|
||||
|
||||
if (!document) {
|
||||
console.log('Kein Dokument gefunden');
|
||||
return null;
|
||||
}
|
||||
if (!document) {
|
||||
console.log('Kein Dokument gefunden');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`Neuestes Dokument gefunden: Version ${document.version}, ID ${document.id}`);
|
||||
return toLocalDocument(document);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden des aktuellen Dokuments:', error);
|
||||
return null;
|
||||
}
|
||||
console.log(`Neuestes Dokument gefunden: Version ${document.version}, ID ${document.id}`);
|
||||
return toLocalDocument(document);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden des aktuellen Dokuments:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle Versionen eines Dokuments für eine Konversation
|
||||
*/
|
||||
export async function getAllDocumentVersions(conversationId: string): Promise<Document[]> {
|
||||
try {
|
||||
console.log(`Lade alle Dokumentversionen für Konversation ${conversationId}`);
|
||||
try {
|
||||
console.log(`Lade alle Dokumentversionen für Konversation ${conversationId}`);
|
||||
|
||||
const documents = await documentApi.getAllDocumentVersions(conversationId);
|
||||
const documents = await documentApi.getAllDocumentVersions(conversationId);
|
||||
|
||||
console.log(`${documents.length} Dokumentversionen geladen`);
|
||||
console.log(`${documents.length} Dokumentversionen geladen`);
|
||||
|
||||
if (documents.length > 0) {
|
||||
console.log(`Erstes Dokument: ID=${documents[0].id}, Version=${documents[0].version}`);
|
||||
} else {
|
||||
console.log('Keine Dokumente gefunden');
|
||||
}
|
||||
if (documents.length > 0) {
|
||||
console.log(`Erstes Dokument: ID=${documents[0].id}, Version=${documents[0].version}`);
|
||||
} else {
|
||||
console.log('Keine Dokumente gefunden');
|
||||
}
|
||||
|
||||
return documents.map(toLocalDocument);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Dokumentversionen:', error);
|
||||
return [];
|
||||
}
|
||||
return documents.map(toLocalDocument);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Dokumentversionen:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob für eine Konversation ein Dokument existiert
|
||||
*/
|
||||
export async function hasDocument(conversationId: string): Promise<boolean> {
|
||||
try {
|
||||
return await documentApi.hasDocument(conversationId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Prüfen auf Dokument:', error);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await documentApi.hasDocument(conversationId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Prüfen auf Dokument:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht eine spezifische Dokumentversion
|
||||
*/
|
||||
export async function deleteDocumentVersion(documentId: string): Promise<boolean> {
|
||||
try {
|
||||
console.log(`=== LÖSCH-OPERATION GESTARTET FÜR DOKUMENT ID ${documentId} ===`);
|
||||
try {
|
||||
console.log(`=== LÖSCH-OPERATION GESTARTET FÜR DOKUMENT ID ${documentId} ===`);
|
||||
|
||||
const success = await documentApi.deleteDocumentVersion(documentId);
|
||||
const success = await documentApi.deleteDocumentVersion(documentId);
|
||||
|
||||
if (success) {
|
||||
console.log(`=== DOKUMENT ${documentId} ERFOLGREICH GELÖSCHT ===`);
|
||||
} else {
|
||||
console.error('Fehler beim Löschen der Dokumentversion');
|
||||
}
|
||||
if (success) {
|
||||
console.log(`=== DOKUMENT ${documentId} ERFOLGREICH GELÖSCHT ===`);
|
||||
} else {
|
||||
console.error('Fehler beim Löschen der Dokumentversion');
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error('Unerwarteter Fehler beim Löschen der Dokumentversion:', error);
|
||||
if (error instanceof Error) {
|
||||
console.error('Fehlerstack:', error.stack);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error('Unerwarteter Fehler beim Löschen der Dokumentversion:', error);
|
||||
if (error instanceof Error) {
|
||||
console.error('Fehlerstack:', error.stack);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@ import { availableModels } from '../config/azure';
|
|||
|
||||
// Typendefinition für ein KI-Modell
|
||||
export interface Model {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
parameters?: {
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
provider?: string;
|
||||
deployment?: string;
|
||||
endpoint?: string;
|
||||
api_version?: string;
|
||||
};
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
parameters?: {
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
provider?: string;
|
||||
deployment?: string;
|
||||
endpoint?: string;
|
||||
api_version?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -23,14 +23,14 @@ export interface Model {
|
|||
* @returns Eine Liste von verfügbaren Modellen
|
||||
*/
|
||||
export async function getModels(): Promise<Model[]> {
|
||||
try {
|
||||
// In einer echten Anwendung würde hier eine API-Anfrage erfolgen
|
||||
// Für jetzt verwenden wir die Fallback-Modelle aus der Konfiguration
|
||||
return availableModels;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Modelle:', error);
|
||||
return availableModels; // Fallback auf lokale Modelle
|
||||
}
|
||||
try {
|
||||
// In einer echten Anwendung würde hier eine API-Anfrage erfolgen
|
||||
// Für jetzt verwenden wir die Fallback-Modelle aus der Konfiguration
|
||||
return availableModels;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Modelle:', error);
|
||||
return availableModels; // Fallback auf lokale Modelle
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -39,14 +39,14 @@ export async function getModels(): Promise<Model[]> {
|
|||
* @returns Das Modell oder undefined, wenn nicht gefunden
|
||||
*/
|
||||
export async function getModelById(id: string): Promise<Model | undefined> {
|
||||
try {
|
||||
const models = await getModels();
|
||||
return models.find(model => model.id === id);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen des Modells:', error);
|
||||
// Fallback: Suche in lokalen Modellen
|
||||
return availableModels.find(model => model.id === id);
|
||||
}
|
||||
try {
|
||||
const models = await getModels();
|
||||
return models.find((model) => model.id === id);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen des Modells:', error);
|
||||
// Fallback: Suche in lokalen Modellen
|
||||
return availableModels.find((model) => model.id === id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -54,11 +54,11 @@ export async function getModelById(id: string): Promise<Model | undefined> {
|
|||
* @returns Das Standard-Modell
|
||||
*/
|
||||
export async function getDefaultModel(): Promise<Model> {
|
||||
try {
|
||||
const models = await getModels();
|
||||
return models[0]; // Das erste Modell in der Liste als Standard
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen des Standard-Modells:', error);
|
||||
return availableModels[0]; // Fallback auf lokales Standard-Modell
|
||||
}
|
||||
}
|
||||
try {
|
||||
const models = await getModels();
|
||||
return models[0]; // Das erste Modell in der Liste als Standard
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen des Standard-Modells:', error);
|
||||
return availableModels[0]; // Fallback auf lokales Standard-Modell
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,188 +13,188 @@ export type { TokenUsage };
|
|||
|
||||
// Chat response type (kept for compatibility)
|
||||
export type ChatResponse = {
|
||||
id: string;
|
||||
choices: {
|
||||
content_filter_results?: any;
|
||||
finish_reason: string;
|
||||
index: number;
|
||||
logprobs: any;
|
||||
message?: {
|
||||
content: string;
|
||||
refusal?: any;
|
||||
role: string;
|
||||
};
|
||||
}[];
|
||||
created: number;
|
||||
model: string;
|
||||
object: string;
|
||||
prompt_filter_results?: any[];
|
||||
system_fingerprint?: string;
|
||||
usage: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
completion_tokens_details?: any;
|
||||
prompt_tokens_details?: any;
|
||||
};
|
||||
id: string;
|
||||
choices: {
|
||||
content_filter_results?: any;
|
||||
finish_reason: string;
|
||||
index: number;
|
||||
logprobs: any;
|
||||
message?: {
|
||||
content: string;
|
||||
refusal?: any;
|
||||
role: string;
|
||||
};
|
||||
}[];
|
||||
created: number;
|
||||
model: string;
|
||||
object: string;
|
||||
prompt_filter_results?: any[];
|
||||
system_fingerprint?: string;
|
||||
usage: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
completion_tokens_details?: any;
|
||||
prompt_tokens_details?: any;
|
||||
};
|
||||
};
|
||||
|
||||
// Return type for chat request
|
||||
export type ChatRequestResult = {
|
||||
content: string;
|
||||
usage: TokenUsage;
|
||||
content: string;
|
||||
usage: TokenUsage;
|
||||
};
|
||||
|
||||
// Logging configuration
|
||||
console.log('Chat Service Konfiguration:', {
|
||||
backendUrl: process.env.EXPO_PUBLIC_BACKEND_URL || 'http://localhost:3001',
|
||||
availableModels: availableModels.length,
|
||||
backendUrl: process.env.EXPO_PUBLIC_BACKEND_URL || 'http://localhost:3001',
|
||||
availableModels: availableModels.length,
|
||||
});
|
||||
|
||||
/**
|
||||
* Calculates estimated cost for an LLM request
|
||||
*/
|
||||
export async function calculateTokenCost(
|
||||
promptTokens: number,
|
||||
completionTokens: number,
|
||||
modelId: string
|
||||
promptTokens: number,
|
||||
completionTokens: number,
|
||||
modelId: string
|
||||
): Promise<number> {
|
||||
try {
|
||||
// Get cost settings from model
|
||||
const modelData = await modelApi.getModel(modelId);
|
||||
try {
|
||||
// Get cost settings from model
|
||||
const modelData = await modelApi.getModel(modelId);
|
||||
|
||||
if (!modelData || !modelData.costSettings) {
|
||||
console.warn('Fehler beim Laden der Kosteninformationen, verwende Standardwerte');
|
||||
const promptCost = 0.0001;
|
||||
const completionCost = 0.0002;
|
||||
if (!modelData || !modelData.costSettings) {
|
||||
console.warn('Fehler beim Laden der Kosteninformationen, verwende Standardwerte');
|
||||
const promptCost = 0.0001;
|
||||
const completionCost = 0.0002;
|
||||
|
||||
const cost = (promptTokens * promptCost + completionTokens * completionCost) / 1000;
|
||||
return Number(cost.toFixed(6));
|
||||
}
|
||||
const cost = (promptTokens * promptCost + completionTokens * completionCost) / 1000;
|
||||
return Number(cost.toFixed(6));
|
||||
}
|
||||
|
||||
const promptCost = modelData.costSettings.prompt_per_1k_tokens || 0.0001;
|
||||
const completionCost = modelData.costSettings.completion_per_1k_tokens || 0.0002;
|
||||
const promptCost = modelData.costSettings.prompt_per_1k_tokens || 0.0001;
|
||||
const completionCost = modelData.costSettings.completion_per_1k_tokens || 0.0002;
|
||||
|
||||
const cost = (promptTokens * promptCost + completionTokens * completionCost) / 1000;
|
||||
return Number(cost.toFixed(6));
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Kostenberechnung:', error);
|
||||
return Number(((promptTokens * 0.0001 + completionTokens * 0.0002) / 1000).toFixed(6));
|
||||
}
|
||||
const cost = (promptTokens * promptCost + completionTokens * completionCost) / 1000;
|
||||
return Number(cost.toFixed(6));
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Kostenberechnung:', error);
|
||||
return Number(((promptTokens * 0.0001 + completionTokens * 0.0002) / 1000).toFixed(6));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs token usage to the database
|
||||
*/
|
||||
export async function logTokenUsage(
|
||||
usage: TokenUsage,
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
userId: string,
|
||||
modelId: string
|
||||
usage: TokenUsage,
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
userId: string,
|
||||
modelId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const estimatedCost = await calculateTokenCost(
|
||||
usage.prompt_tokens,
|
||||
usage.completion_tokens,
|
||||
modelId
|
||||
);
|
||||
try {
|
||||
const estimatedCost = await calculateTokenCost(
|
||||
usage.prompt_tokens,
|
||||
usage.completion_tokens,
|
||||
modelId
|
||||
);
|
||||
|
||||
const success = await usageApi.logTokenUsage({
|
||||
conversationId,
|
||||
messageId,
|
||||
modelId,
|
||||
promptTokens: usage.prompt_tokens,
|
||||
completionTokens: usage.completion_tokens,
|
||||
totalTokens: usage.total_tokens,
|
||||
estimatedCost,
|
||||
});
|
||||
const success = await usageApi.logTokenUsage({
|
||||
conversationId,
|
||||
messageId,
|
||||
modelId,
|
||||
promptTokens: usage.prompt_tokens,
|
||||
completionTokens: usage.completion_tokens,
|
||||
totalTokens: usage.total_tokens,
|
||||
estimatedCost,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
console.log('Token-Nutzung erfolgreich gespeichert:', {
|
||||
conversationId,
|
||||
messageId,
|
||||
totalTokens: usage.total_tokens,
|
||||
estimatedCost,
|
||||
});
|
||||
} else {
|
||||
console.error('Fehler beim Speichern der Token-Nutzung');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Loggen der Token-Nutzung:', error);
|
||||
}
|
||||
if (success) {
|
||||
console.log('Token-Nutzung erfolgreich gespeichert:', {
|
||||
conversationId,
|
||||
messageId,
|
||||
totalTokens: usage.total_tokens,
|
||||
estimatedCost,
|
||||
});
|
||||
} else {
|
||||
console.error('Fehler beim Speichern der Token-Nutzung');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Loggen der Token-Nutzung:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a chat request via the backend
|
||||
*/
|
||||
export async function sendChatRequest(
|
||||
messages: ChatMessage[],
|
||||
temperature: number = 0.7,
|
||||
maxTokens: number = 800
|
||||
messages: ChatMessage[],
|
||||
temperature: number = 0.7,
|
||||
maxTokens: number = 800
|
||||
): Promise<string | ChatRequestResult> {
|
||||
console.log('sendChatRequest gestartet mit:', {
|
||||
messagesCount: messages.length,
|
||||
maxTokens,
|
||||
});
|
||||
console.log('sendChatRequest gestartet mit:', {
|
||||
messagesCount: messages.length,
|
||||
maxTokens,
|
||||
});
|
||||
|
||||
try {
|
||||
// Find model deployment from system message
|
||||
let modelId = '550e8400-e29b-41d4-a716-446655440000'; // Default to GPT-O3-Mini
|
||||
try {
|
||||
// Find model deployment from system message
|
||||
let modelId = '550e8400-e29b-41d4-a716-446655440000'; // Default to GPT-O3-Mini
|
||||
|
||||
const systemMessage = messages.find(
|
||||
(msg) => msg.role === 'system' && msg.content.startsWith('MODEL:')
|
||||
);
|
||||
if (systemMessage) {
|
||||
const deployment = systemMessage.content.split(':')[1].trim();
|
||||
console.log('Modell in system Nachricht erkannt:', deployment);
|
||||
const systemMessage = messages.find(
|
||||
(msg) => msg.role === 'system' && msg.content.startsWith('MODEL:')
|
||||
);
|
||||
if (systemMessage) {
|
||||
const deployment = systemMessage.content.split(':')[1].trim();
|
||||
console.log('Modell in system Nachricht erkannt:', deployment);
|
||||
|
||||
// Map deployment to model ID
|
||||
const deploymentToModelId: Record<string, string> = {
|
||||
'gpt-o3-mini-se': '550e8400-e29b-41d4-a716-446655440000',
|
||||
'gpt-4o-mini-se': '550e8400-e29b-41d4-a716-446655440004',
|
||||
'gpt-4o-se': '550e8400-e29b-41d4-a716-446655440005',
|
||||
};
|
||||
// Map deployment to model ID
|
||||
const deploymentToModelId: Record<string, string> = {
|
||||
'gpt-o3-mini-se': '550e8400-e29b-41d4-a716-446655440000',
|
||||
'gpt-4o-mini-se': '550e8400-e29b-41d4-a716-446655440004',
|
||||
'gpt-4o-se': '550e8400-e29b-41d4-a716-446655440005',
|
||||
};
|
||||
|
||||
modelId = deploymentToModelId[deployment] || modelId;
|
||||
} else {
|
||||
console.warn('Keine System-Nachricht mit MODEL-Präfix gefunden!');
|
||||
}
|
||||
modelId = deploymentToModelId[deployment] || modelId;
|
||||
} else {
|
||||
console.warn('Keine System-Nachricht mit MODEL-Präfix gefunden!');
|
||||
}
|
||||
|
||||
console.log('Verwende Model ID:', modelId);
|
||||
console.log('Verwende Model ID:', modelId);
|
||||
|
||||
// Filter out MODEL: system messages before sending to API
|
||||
const filteredMessages = messages.filter(
|
||||
(msg) => !(msg.role === 'system' && msg.content.startsWith('MODEL:'))
|
||||
);
|
||||
// Filter out MODEL: system messages before sending to API
|
||||
const filteredMessages = messages.filter(
|
||||
(msg) => !(msg.role === 'system' && msg.content.startsWith('MODEL:'))
|
||||
);
|
||||
|
||||
// Send request to backend
|
||||
const result = await chatApi.createCompletion({
|
||||
messages: filteredMessages,
|
||||
modelId,
|
||||
temperature,
|
||||
maxTokens,
|
||||
});
|
||||
// Send request to backend
|
||||
const result = await chatApi.createCompletion({
|
||||
messages: filteredMessages,
|
||||
modelId,
|
||||
temperature,
|
||||
maxTokens,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return 'Es tut mir leid, aber ich konnte keine Antwort generieren. Bitte stelle sicher, dass das Backend läuft.';
|
||||
}
|
||||
if (!result) {
|
||||
return 'Es tut mir leid, aber ich konnte keine Antwort generieren. Bitte stelle sicher, dass das Backend läuft.';
|
||||
}
|
||||
|
||||
return {
|
||||
content: result.content,
|
||||
usage: result.usage,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Chat-Anfrage:', error);
|
||||
return {
|
||||
content: result.content,
|
||||
usage: result.usage,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Chat-Anfrage:', error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
console.error('Fehlerdetails:', {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
console.error('Fehlerdetails:', {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
|
||||
return `Es tut mir leid, aber ich konnte keine Antwort generieren. Bitte stelle sicher, dass das Backend läuft. Fehlerdetails: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`;
|
||||
}
|
||||
return `Es tut mir leid, aber ich konnte keine Antwort generieren. Bitte stelle sicher, dass das Backend läuft. Fehlerdetails: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,249 +5,249 @@ import { spaceApi, type Space as ApiSpace, type SpaceMember as ApiSpaceMember }
|
|||
|
||||
// Re-export types with backwards-compatible naming (snake_case for mobile)
|
||||
export type Space = {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
owner_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
is_archived: boolean;
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
owner_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
is_archived: boolean;
|
||||
};
|
||||
|
||||
export type SpaceMember = {
|
||||
id: string;
|
||||
space_id: string;
|
||||
user_id: string;
|
||||
role: 'owner' | 'admin' | 'member' | 'viewer';
|
||||
invitation_status: 'pending' | 'accepted' | 'declined';
|
||||
invited_by?: string;
|
||||
invited_at: string;
|
||||
joined_at?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
id: string;
|
||||
space_id: string;
|
||||
user_id: string;
|
||||
role: 'owner' | 'admin' | 'member' | 'viewer';
|
||||
invitation_status: 'pending' | 'accepted' | 'declined';
|
||||
invited_by?: string;
|
||||
invited_at: string;
|
||||
joined_at?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
// Helper to convert API response to local format
|
||||
function toLocalSpace(space: ApiSpace): Space {
|
||||
return {
|
||||
id: space.id,
|
||||
name: space.name,
|
||||
description: space.description,
|
||||
owner_id: space.ownerId,
|
||||
created_at: space.createdAt,
|
||||
updated_at: space.updatedAt,
|
||||
is_archived: space.isArchived,
|
||||
};
|
||||
return {
|
||||
id: space.id,
|
||||
name: space.name,
|
||||
description: space.description,
|
||||
owner_id: space.ownerId,
|
||||
created_at: space.createdAt,
|
||||
updated_at: space.updatedAt,
|
||||
is_archived: space.isArchived,
|
||||
};
|
||||
}
|
||||
|
||||
function toLocalSpaceMember(member: ApiSpaceMember): SpaceMember {
|
||||
return {
|
||||
id: member.id,
|
||||
space_id: member.spaceId,
|
||||
user_id: member.userId,
|
||||
role: member.role,
|
||||
invitation_status: member.invitationStatus,
|
||||
invited_by: member.invitedBy,
|
||||
invited_at: member.invitedAt,
|
||||
joined_at: member.joinedAt,
|
||||
created_at: member.createdAt,
|
||||
updated_at: member.updatedAt,
|
||||
};
|
||||
return {
|
||||
id: member.id,
|
||||
space_id: member.spaceId,
|
||||
user_id: member.userId,
|
||||
role: member.role,
|
||||
invitation_status: member.invitationStatus,
|
||||
invited_by: member.invitedBy,
|
||||
invited_at: member.invitedAt,
|
||||
joined_at: member.joinedAt,
|
||||
created_at: member.createdAt,
|
||||
updated_at: member.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all spaces for a user (both owned and member of)
|
||||
*/
|
||||
export async function getUserSpaces(userId: string): Promise<Space[]> {
|
||||
try {
|
||||
const spaces = await spaceApi.getUserSpaces();
|
||||
return spaces.map(toLocalSpace);
|
||||
} catch (error) {
|
||||
console.error('Error in getUserSpaces:', error);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const spaces = await spaceApi.getUserSpaces();
|
||||
return spaces.map(toLocalSpace);
|
||||
} catch (error) {
|
||||
console.error('Error in getUserSpaces:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get spaces the user owns
|
||||
*/
|
||||
export async function getOwnedSpaces(userId: string): Promise<Space[]> {
|
||||
try {
|
||||
const spaces = await spaceApi.getOwnedSpaces();
|
||||
return spaces.map(toLocalSpace);
|
||||
} catch (error) {
|
||||
console.error('Error in getOwnedSpaces:', error);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const spaces = await spaceApi.getOwnedSpaces();
|
||||
return spaces.map(toLocalSpace);
|
||||
} catch (error) {
|
||||
console.error('Error in getOwnedSpaces:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single space by ID
|
||||
*/
|
||||
export async function getSpace(spaceId: string): Promise<Space | null> {
|
||||
try {
|
||||
const space = await spaceApi.getSpace(spaceId);
|
||||
if (!space) {
|
||||
return null;
|
||||
}
|
||||
return toLocalSpace(space);
|
||||
} catch (error) {
|
||||
console.error('Error in getSpace:', error);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const space = await spaceApi.getSpace(spaceId);
|
||||
if (!space) {
|
||||
return null;
|
||||
}
|
||||
return toLocalSpace(space);
|
||||
} catch (error) {
|
||||
console.error('Error in getSpace:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new space
|
||||
*/
|
||||
export async function createSpace(
|
||||
userId: string,
|
||||
name: string,
|
||||
description?: string
|
||||
userId: string,
|
||||
name: string,
|
||||
description?: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const space = await spaceApi.createSpace(name, description);
|
||||
return space?.id || null;
|
||||
} catch (error) {
|
||||
console.error('Error in createSpace:', error);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const space = await spaceApi.createSpace(name, description);
|
||||
return space?.id || null;
|
||||
} catch (error) {
|
||||
console.error('Error in createSpace:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a space
|
||||
*/
|
||||
export async function updateSpace(
|
||||
spaceId: string,
|
||||
updates: { name?: string; description?: string; is_archived?: boolean }
|
||||
spaceId: string,
|
||||
updates: { name?: string; description?: string; is_archived?: boolean }
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
return await spaceApi.updateSpace(spaceId, {
|
||||
name: updates.name,
|
||||
description: updates.description,
|
||||
isArchived: updates.is_archived,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in updateSpace:', error);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await spaceApi.updateSpace(spaceId, {
|
||||
name: updates.name,
|
||||
description: updates.description,
|
||||
isArchived: updates.is_archived,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in updateSpace:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a space
|
||||
*/
|
||||
export async function deleteSpace(spaceId: string): Promise<boolean> {
|
||||
try {
|
||||
return await spaceApi.deleteSpace(spaceId);
|
||||
} catch (error) {
|
||||
console.error('Error in deleteSpace:', error);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await spaceApi.deleteSpace(spaceId);
|
||||
} catch (error) {
|
||||
console.error('Error in deleteSpace:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get members of a space
|
||||
*/
|
||||
export async function getSpaceMembers(spaceId: string): Promise<SpaceMember[]> {
|
||||
try {
|
||||
const members = await spaceApi.getSpaceMembers(spaceId);
|
||||
return members.map(toLocalSpaceMember);
|
||||
} catch (error) {
|
||||
console.error('Error in getSpaceMembers:', error);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const members = await spaceApi.getSpaceMembers(spaceId);
|
||||
return members.map(toLocalSpaceMember);
|
||||
} catch (error) {
|
||||
console.error('Error in getSpaceMembers:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a member to a space (invite)
|
||||
*/
|
||||
export async function inviteUserToSpace(
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
invitedByUserId: string,
|
||||
role: 'admin' | 'member' | 'viewer' = 'member'
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
invitedByUserId: string,
|
||||
role: 'admin' | 'member' | 'viewer' = 'member'
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
return await spaceApi.inviteUser(spaceId, userId, role);
|
||||
} catch (error) {
|
||||
console.error('Error in inviteUserToSpace:', error);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await spaceApi.inviteUser(spaceId, userId, role);
|
||||
} catch (error) {
|
||||
console.error('Error in inviteUserToSpace:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept or decline a space invitation
|
||||
*/
|
||||
export async function respondToInvitation(
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
status: 'accepted' | 'declined'
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
status: 'accepted' | 'declined'
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
return await spaceApi.respondToInvitation(spaceId, status);
|
||||
} catch (error) {
|
||||
console.error('Error in respondToInvitation:', error);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await spaceApi.respondToInvitation(spaceId, status);
|
||||
} catch (error) {
|
||||
console.error('Error in respondToInvitation:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a member from a space
|
||||
*/
|
||||
export async function removeMember(spaceId: string, userId: string): Promise<boolean> {
|
||||
try {
|
||||
return await spaceApi.removeMember(spaceId, userId);
|
||||
} catch (error) {
|
||||
console.error('Error in removeMember:', error);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await spaceApi.removeMember(spaceId, userId);
|
||||
} catch (error) {
|
||||
console.error('Error in removeMember:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a member's role
|
||||
*/
|
||||
export async function changeMemberRole(
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
newRole: 'admin' | 'member' | 'viewer'
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
newRole: 'admin' | 'member' | 'viewer'
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
return await spaceApi.changeMemberRole(spaceId, userId, newRole);
|
||||
} catch (error) {
|
||||
console.error('Error in changeMemberRole:', error);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await spaceApi.changeMemberRole(spaceId, userId, newRole);
|
||||
} catch (error) {
|
||||
console.error('Error in changeMemberRole:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's role in a space
|
||||
*/
|
||||
export async function getUserRoleInSpace(
|
||||
spaceId: string,
|
||||
userId: string
|
||||
spaceId: string,
|
||||
userId: string
|
||||
): Promise<'owner' | 'admin' | 'member' | 'viewer' | null> {
|
||||
try {
|
||||
return await spaceApi.getUserRoleInSpace(spaceId);
|
||||
} catch (error) {
|
||||
console.error('Error in getUserRoleInSpace:', error);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return await spaceApi.getUserRoleInSpace(spaceId);
|
||||
} catch (error) {
|
||||
console.error('Error in getUserRoleInSpace:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending space invitations for a user
|
||||
*/
|
||||
export async function getPendingInvitations(
|
||||
userId: string
|
||||
userId: string
|
||||
): Promise<Array<{ invitation: SpaceMember; space: Space }>> {
|
||||
try {
|
||||
const invitations = await spaceApi.getPendingInvitations();
|
||||
return invitations.map((inv) => ({
|
||||
invitation: toLocalSpaceMember(inv.invitation),
|
||||
space: toLocalSpace(inv.space),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error in getPendingInvitations:', error);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const invitations = await spaceApi.getPendingInvitations();
|
||||
return invitations.map((inv) => ({
|
||||
invitation: toLocalSpaceMember(inv.invitation),
|
||||
space: toLocalSpace(inv.space),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error in getPendingInvitations:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,159 +5,159 @@ import { templateApi, type Template as ApiTemplate } from './api';
|
|||
|
||||
// Re-export type with backwards-compatible naming (snake_case for mobile)
|
||||
export interface Template {
|
||||
id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
system_prompt: string;
|
||||
initial_question: string | null;
|
||||
model_id: string | null;
|
||||
color: string;
|
||||
is_default: boolean;
|
||||
document_mode: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
system_prompt: string;
|
||||
initial_question: string | null;
|
||||
model_id: string | null;
|
||||
color: string;
|
||||
is_default: boolean;
|
||||
document_mode: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// Helper to convert API response to local format
|
||||
function toLocalTemplate(template: ApiTemplate): Template {
|
||||
return {
|
||||
id: template.id,
|
||||
user_id: template.userId,
|
||||
name: template.name,
|
||||
description: template.description || null,
|
||||
system_prompt: template.systemPrompt,
|
||||
initial_question: template.initialQuestion || null,
|
||||
model_id: template.modelId || null,
|
||||
color: template.color,
|
||||
is_default: template.isDefault,
|
||||
document_mode: template.documentMode,
|
||||
created_at: template.createdAt,
|
||||
updated_at: template.updatedAt,
|
||||
};
|
||||
return {
|
||||
id: template.id,
|
||||
user_id: template.userId,
|
||||
name: template.name,
|
||||
description: template.description || null,
|
||||
system_prompt: template.systemPrompt,
|
||||
initial_question: template.initialQuestion || null,
|
||||
model_id: template.modelId || null,
|
||||
color: template.color,
|
||||
is_default: template.isDefault,
|
||||
document_mode: template.documentMode,
|
||||
created_at: template.createdAt,
|
||||
updated_at: template.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle Vorlagen eines Benutzers
|
||||
*/
|
||||
export async function getTemplates(userId: string): Promise<Template[]> {
|
||||
try {
|
||||
const templates = await templateApi.getTemplates();
|
||||
return templates.map(toLocalTemplate);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Vorlagen:', error);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const templates = await templateApi.getTemplates();
|
||||
return templates.map(toLocalTemplate);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Vorlagen:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt eine bestimmte Vorlage anhand ihrer ID
|
||||
*/
|
||||
export async function getTemplateById(templateId: string): Promise<Template | null> {
|
||||
try {
|
||||
const template = await templateApi.getTemplate(templateId);
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
return toLocalTemplate(template);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Vorlage:', error);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const template = await templateApi.getTemplate(templateId);
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
return toLocalTemplate(template);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Vorlage:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine neue Vorlage
|
||||
*/
|
||||
export async function createTemplate(
|
||||
template: Omit<Template, 'id' | 'created_at' | 'updated_at'>
|
||||
template: Omit<Template, 'id' | 'created_at' | 'updated_at'>
|
||||
): Promise<Template | null> {
|
||||
try {
|
||||
const result = await templateApi.createTemplate({
|
||||
name: template.name,
|
||||
description: template.description || undefined,
|
||||
systemPrompt: template.system_prompt,
|
||||
initialQuestion: template.initial_question || undefined,
|
||||
modelId: template.model_id || undefined,
|
||||
color: template.color,
|
||||
documentMode: template.document_mode,
|
||||
});
|
||||
try {
|
||||
const result = await templateApi.createTemplate({
|
||||
name: template.name,
|
||||
description: template.description || undefined,
|
||||
systemPrompt: template.system_prompt,
|
||||
initialQuestion: template.initial_question || undefined,
|
||||
modelId: template.model_id || undefined,
|
||||
color: template.color,
|
||||
documentMode: template.document_mode,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
console.error('Fehler beim Erstellen der Vorlage');
|
||||
return null;
|
||||
}
|
||||
if (!result) {
|
||||
console.error('Fehler beim Erstellen der Vorlage');
|
||||
return null;
|
||||
}
|
||||
|
||||
return toLocalTemplate(result);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Vorlage:', error);
|
||||
return null;
|
||||
}
|
||||
return toLocalTemplate(result);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Vorlage:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert eine bestehende Vorlage
|
||||
*/
|
||||
export async function updateTemplate(
|
||||
templateId: string,
|
||||
updates: Partial<Omit<Template, 'id' | 'user_id' | 'created_at' | 'updated_at'>>
|
||||
templateId: string,
|
||||
updates: Partial<Omit<Template, 'id' | 'user_id' | 'created_at' | 'updated_at'>>
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const apiUpdates: Parameters<typeof templateApi.updateTemplate>[1] = {};
|
||||
try {
|
||||
const apiUpdates: Parameters<typeof templateApi.updateTemplate>[1] = {};
|
||||
|
||||
if (updates.name !== undefined) apiUpdates.name = updates.name;
|
||||
if (updates.description !== undefined)
|
||||
apiUpdates.description = updates.description || undefined;
|
||||
if (updates.system_prompt !== undefined) apiUpdates.systemPrompt = updates.system_prompt;
|
||||
if (updates.initial_question !== undefined)
|
||||
apiUpdates.initialQuestion = updates.initial_question || undefined;
|
||||
if (updates.model_id !== undefined) apiUpdates.modelId = updates.model_id || undefined;
|
||||
if (updates.color !== undefined) apiUpdates.color = updates.color;
|
||||
if (updates.document_mode !== undefined) apiUpdates.documentMode = updates.document_mode;
|
||||
if (updates.name !== undefined) apiUpdates.name = updates.name;
|
||||
if (updates.description !== undefined)
|
||||
apiUpdates.description = updates.description || undefined;
|
||||
if (updates.system_prompt !== undefined) apiUpdates.systemPrompt = updates.system_prompt;
|
||||
if (updates.initial_question !== undefined)
|
||||
apiUpdates.initialQuestion = updates.initial_question || undefined;
|
||||
if (updates.model_id !== undefined) apiUpdates.modelId = updates.model_id || undefined;
|
||||
if (updates.color !== undefined) apiUpdates.color = updates.color;
|
||||
if (updates.document_mode !== undefined) apiUpdates.documentMode = updates.document_mode;
|
||||
|
||||
return await templateApi.updateTemplate(templateId, apiUpdates);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Vorlage:', error);
|
||||
return false;
|
||||
}
|
||||
return await templateApi.updateTemplate(templateId, apiUpdates);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Vorlage:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht eine Vorlage
|
||||
*/
|
||||
export async function deleteTemplate(templateId: string): Promise<boolean> {
|
||||
try {
|
||||
return await templateApi.deleteTemplate(templateId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Vorlage:', error);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await templateApi.deleteTemplate(templateId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen der Vorlage:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt eine Vorlage als Standard
|
||||
*/
|
||||
export async function setDefaultTemplate(templateId: string, userId: string): Promise<boolean> {
|
||||
try {
|
||||
return await templateApi.setDefaultTemplate(templateId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Setzen der Standard-Vorlage:', error);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await templateApi.setDefaultTemplate(templateId);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Setzen der Standard-Vorlage:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt die Standard-Vorlage des Benutzers
|
||||
*/
|
||||
export async function getDefaultTemplate(userId: string): Promise<Template | null> {
|
||||
try {
|
||||
const template = await templateApi.getDefaultTemplate();
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
return toLocalTemplate(template);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Standard-Vorlage:', error);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const template = await templateApi.getDefaultTemplate();
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
return toLocalTemplate(template);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Standard-Vorlage:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,59 +1,59 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./app/**/*.{js,ts,tsx}', './components/**/*.{js,ts,tsx}'],
|
||||
presets: [require('nativewind/preset')],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Light Mode Colors
|
||||
primary: '#0A84FF',
|
||||
secondary: '#5E5CE6',
|
||||
background: '#F2F2F7',
|
||||
card: '#FFFFFF',
|
||||
textColor: '#000000',
|
||||
border: '#E5E5EA',
|
||||
notification: '#FF3B30',
|
||||
placeholder: '#8E8E93',
|
||||
accent: '#34C759',
|
||||
muted: '#C7C7CC',
|
||||
|
||||
// Dark Mode Colors für die dark: Variante
|
||||
dark: {
|
||||
primary: '#0A84FF',
|
||||
secondary: '#5E5CE6',
|
||||
background: '#1C1C1E',
|
||||
card: '#2C2C2E',
|
||||
textColor: '#FFFFFF',
|
||||
border: '#38383A',
|
||||
notification: '#FF453A',
|
||||
placeholder: '#8E8E93',
|
||||
accent: '#30D158',
|
||||
muted: '#48484A',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['System', 'sans-serif'],
|
||||
},
|
||||
spacing: {
|
||||
xs: '4px',
|
||||
sm: '8px',
|
||||
md: '16px',
|
||||
lg: '24px',
|
||||
xl: '32px',
|
||||
'2xl': '48px',
|
||||
'3xl': '64px',
|
||||
},
|
||||
borderRadius: {
|
||||
none: '0',
|
||||
sm: '4px',
|
||||
DEFAULT: '8px',
|
||||
md: '12px',
|
||||
lg: '16px',
|
||||
xl: '24px',
|
||||
full: '9999px',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
content: ['./app/**/*.{js,ts,tsx}', './components/**/*.{js,ts,tsx}'],
|
||||
presets: [require('nativewind/preset')],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Light Mode Colors
|
||||
primary: '#0A84FF',
|
||||
secondary: '#5E5CE6',
|
||||
background: '#F2F2F7',
|
||||
card: '#FFFFFF',
|
||||
textColor: '#000000',
|
||||
border: '#E5E5EA',
|
||||
notification: '#FF3B30',
|
||||
placeholder: '#8E8E93',
|
||||
accent: '#34C759',
|
||||
muted: '#C7C7CC',
|
||||
|
||||
// Dark Mode Colors für die dark: Variante
|
||||
dark: {
|
||||
primary: '#0A84FF',
|
||||
secondary: '#5E5CE6',
|
||||
background: '#1C1C1E',
|
||||
card: '#2C2C2E',
|
||||
textColor: '#FFFFFF',
|
||||
border: '#38383A',
|
||||
notification: '#FF453A',
|
||||
placeholder: '#8E8E93',
|
||||
accent: '#30D158',
|
||||
muted: '#48484A',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['System', 'sans-serif'],
|
||||
},
|
||||
spacing: {
|
||||
xs: '4px',
|
||||
sm: '8px',
|
||||
md: '16px',
|
||||
lg: '24px',
|
||||
xl: '32px',
|
||||
'2xl': '48px',
|
||||
'3xl': '64px',
|
||||
},
|
||||
borderRadius: {
|
||||
none: '0',
|
||||
sm: '4px',
|
||||
DEFAULT: '8px',
|
||||
md: '12px',
|
||||
lg: '16px',
|
||||
xl: '24px',
|
||||
full: '9999px',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,79 +2,92 @@
|
|||
const fetch = require('node-fetch');
|
||||
|
||||
async function testAzureOpenAI() {
|
||||
const config = {
|
||||
endpoint: 'https://memoroseopenai.openai.azure.com',
|
||||
deployment: 'gpt-o3-mini-se',
|
||||
apiVersion: '2024-12-01-preview',
|
||||
apiKey: '3082103c9b0d4270a795686ccaa89921',
|
||||
};
|
||||
const config = {
|
||||
endpoint: 'https://memoroseopenai.openai.azure.com',
|
||||
deployment: 'gpt-o3-mini-se',
|
||||
apiVersion: '2024-12-01-preview',
|
||||
apiKey: '3082103c9b0d4270a795686ccaa89921',
|
||||
};
|
||||
|
||||
const url = `${config.endpoint}/openai/deployments/${config.deployment}/chat/completions?api-version=${config.apiVersion}`;
|
||||
|
||||
console.log('Sende Anfrage an:', url);
|
||||
console.log('API-Key (gekürzt):', config.apiKey.substring(0, 5) + '...' + config.apiKey.substring(config.apiKey.length - 5));
|
||||
|
||||
const requestBody = {
|
||||
messages: [
|
||||
{ role: 'system', content: 'Du bist ein hilfreicher Assistent.' },
|
||||
{ role: 'user', content: 'Hallo, wie geht es dir?' }
|
||||
],
|
||||
max_completion_tokens: 800,
|
||||
};
|
||||
|
||||
console.log('Request Body:', JSON.stringify(requestBody, null, 2));
|
||||
|
||||
try {
|
||||
console.log('Sende Anfrage...');
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'api-key': config.apiKey,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
const url = `${config.endpoint}/openai/deployments/${config.deployment}/chat/completions?api-version=${config.apiVersion}`;
|
||||
|
||||
console.log('Antwort-Status:', response.status, response.statusText);
|
||||
console.log('Antwort-Headers:', JSON.stringify(Object.fromEntries([...response.headers.entries()]), null, 2));
|
||||
|
||||
const responseText = await response.text();
|
||||
console.log('Antwort-Text-Länge:', responseText.length);
|
||||
console.log('Antwort-Text (gekürzt):', responseText.length > 1000 ?
|
||||
responseText.substring(0, 500) + '\n...\n' + responseText.substring(responseText.length - 500) :
|
||||
responseText);
|
||||
|
||||
try {
|
||||
const data = JSON.parse(responseText);
|
||||
console.log('Geparste Antwort:', JSON.stringify(data, null, 2));
|
||||
|
||||
if (data.choices && data.choices.length > 0) {
|
||||
const message = data.choices[0].message;
|
||||
if (message && message.content) {
|
||||
console.log('Antwort des Assistenten:', message.content);
|
||||
} else {
|
||||
console.error('Unerwartetes Antwortformat:', data.choices[0]);
|
||||
}
|
||||
} else {
|
||||
console.error('Keine choices in der Antwort gefunden');
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error('Fehler beim Parsen der Antwort:', parseError);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der API-Anfrage:', error);
|
||||
if (error.response) {
|
||||
console.error('Fehler-Response:', await error.response.text());
|
||||
}
|
||||
console.error('Stack:', error.stack);
|
||||
}
|
||||
console.log('Sende Anfrage an:', url);
|
||||
console.log(
|
||||
'API-Key (gekürzt):',
|
||||
config.apiKey.substring(0, 5) + '...' + config.apiKey.substring(config.apiKey.length - 5)
|
||||
);
|
||||
|
||||
const requestBody = {
|
||||
messages: [
|
||||
{ role: 'system', content: 'Du bist ein hilfreicher Assistent.' },
|
||||
{ role: 'user', content: 'Hallo, wie geht es dir?' },
|
||||
],
|
||||
max_completion_tokens: 800,
|
||||
};
|
||||
|
||||
console.log('Request Body:', JSON.stringify(requestBody, null, 2));
|
||||
|
||||
try {
|
||||
console.log('Sende Anfrage...');
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'api-key': config.apiKey,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
console.log('Antwort-Status:', response.status, response.statusText);
|
||||
console.log(
|
||||
'Antwort-Headers:',
|
||||
JSON.stringify(Object.fromEntries([...response.headers.entries()]), null, 2)
|
||||
);
|
||||
|
||||
const responseText = await response.text();
|
||||
console.log('Antwort-Text-Länge:', responseText.length);
|
||||
console.log(
|
||||
'Antwort-Text (gekürzt):',
|
||||
responseText.length > 1000
|
||||
? responseText.substring(0, 500) +
|
||||
'\n...\n' +
|
||||
responseText.substring(responseText.length - 500)
|
||||
: responseText
|
||||
);
|
||||
|
||||
try {
|
||||
const data = JSON.parse(responseText);
|
||||
console.log('Geparste Antwort:', JSON.stringify(data, null, 2));
|
||||
|
||||
if (data.choices && data.choices.length > 0) {
|
||||
const message = data.choices[0].message;
|
||||
if (message && message.content) {
|
||||
console.log('Antwort des Assistenten:', message.content);
|
||||
} else {
|
||||
console.error('Unerwartetes Antwortformat:', data.choices[0]);
|
||||
}
|
||||
} else {
|
||||
console.error('Keine choices in der Antwort gefunden');
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error('Fehler beim Parsen der Antwort:', parseError);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der API-Anfrage:', error);
|
||||
if (error.response) {
|
||||
console.error('Fehler-Response:', await error.response.text());
|
||||
}
|
||||
console.error('Stack:', error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
// Führe den Test aus
|
||||
testAzureOpenAI().then(() => {
|
||||
console.log('Test abgeschlossen');
|
||||
}).catch(err => {
|
||||
console.error('Unbehandelter Fehler:', err);
|
||||
});
|
||||
testAzureOpenAI()
|
||||
.then(() => {
|
||||
console.log('Test abgeschlossen');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Unbehandelter Fehler:', err);
|
||||
});
|
||||
|
||||
testAzureOpenAI();
|
||||
|
|
|
|||
|
|
@ -2,49 +2,49 @@
|
|||
const fetch = require('node-fetch');
|
||||
|
||||
async function testAzureOpenAI() {
|
||||
const apiKey = '3082103c9b0d4270a795686ccaa89921';
|
||||
const endpoint = 'https://memoroseopenai.openai.azure.com';
|
||||
const deployment = 'gpt-o3-mini-se';
|
||||
const apiVersion = '2024-12-01-preview';
|
||||
const apiKey = '3082103c9b0d4270a795686ccaa89921';
|
||||
const endpoint = 'https://memoroseopenai.openai.azure.com';
|
||||
const deployment = 'gpt-o3-mini-se';
|
||||
const apiVersion = '2024-12-01-preview';
|
||||
|
||||
const url = `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}`;
|
||||
|
||||
const messages = [
|
||||
{ role: "system", content: "You are a helpful assistant." },
|
||||
{ role: "user", content: "Hello, who are you?" }
|
||||
];
|
||||
|
||||
try {
|
||||
console.log("Sending request to:", url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'api-key': apiKey
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: messages,
|
||||
max_completion_tokens: 800
|
||||
})
|
||||
});
|
||||
|
||||
const status = response.status;
|
||||
console.log("Response status:", status);
|
||||
|
||||
const data = await response.text();
|
||||
console.log("Response data:", data);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Error:", data);
|
||||
} else {
|
||||
const jsonData = JSON.parse(data);
|
||||
console.log("Content:", jsonData.choices[0].message.content);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Fetch error:", error);
|
||||
}
|
||||
const url = `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}`;
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: 'You are a helpful assistant.' },
|
||||
{ role: 'user', content: 'Hello, who are you?' },
|
||||
];
|
||||
|
||||
try {
|
||||
console.log('Sending request to:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'api-key': apiKey,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: messages,
|
||||
max_completion_tokens: 800,
|
||||
}),
|
||||
});
|
||||
|
||||
const status = response.status;
|
||||
console.log('Response status:', status);
|
||||
|
||||
const data = await response.text();
|
||||
console.log('Response data:', data);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Error:', data);
|
||||
} else {
|
||||
const jsonData = JSON.parse(data);
|
||||
console.log('Content:', jsonData.choices[0].message.content);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test
|
||||
testAzureOpenAI().catch(console.error);
|
||||
testAzureOpenAI().catch(console.error);
|
||||
|
|
|
|||
|
|
@ -4,38 +4,38 @@ import { lightTheme, darkTheme } from './index';
|
|||
import { Theme } from '@react-navigation/native';
|
||||
|
||||
type ThemeContextType = {
|
||||
isDarkMode: boolean;
|
||||
toggleTheme: () => void;
|
||||
theme: Theme;
|
||||
isDarkMode: boolean;
|
||||
toggleTheme: () => void;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType>({
|
||||
isDarkMode: false,
|
||||
toggleTheme: () => {},
|
||||
theme: lightTheme,
|
||||
isDarkMode: false,
|
||||
toggleTheme: () => {},
|
||||
theme: lightTheme,
|
||||
});
|
||||
|
||||
export const useAppTheme = () => useContext(ThemeContext);
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const colorScheme = useColorScheme();
|
||||
const [isDarkMode, setIsDarkMode] = useState(colorScheme === 'dark');
|
||||
const colorScheme = useColorScheme();
|
||||
const [isDarkMode, setIsDarkMode] = useState(colorScheme === 'dark');
|
||||
|
||||
// Aktualisiere den Theme-Modus, wenn sich das System-Theme ändert
|
||||
useEffect(() => {
|
||||
setIsDarkMode(colorScheme === 'dark');
|
||||
}, [colorScheme]);
|
||||
// Aktualisiere den Theme-Modus, wenn sich das System-Theme ändert
|
||||
useEffect(() => {
|
||||
setIsDarkMode(colorScheme === 'dark');
|
||||
}, [colorScheme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setIsDarkMode((prev) => !prev);
|
||||
};
|
||||
const toggleTheme = () => {
|
||||
setIsDarkMode((prev) => !prev);
|
||||
};
|
||||
|
||||
const theme = isDarkMode ? darkTheme : lightTheme;
|
||||
const theme = isDarkMode ? darkTheme : lightTheme;
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ isDarkMode, toggleTheme, theme }}>
|
||||
{/* Wir verwenden die NativeWind-Klassen direkt in den Komponenten */}
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
return (
|
||||
<ThemeContext.Provider value={{ isDarkMode, toggleTheme, theme }}>
|
||||
{/* Wir verwenden die NativeWind-Klassen direkt in den Komponenten */}
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,83 +1,83 @@
|
|||
import { DefaultTheme, DarkTheme } from '@react-navigation/native';
|
||||
|
||||
export const lightTheme = {
|
||||
...DefaultTheme,
|
||||
colors: {
|
||||
...DefaultTheme.colors,
|
||||
primary: '#0A84FF',
|
||||
secondary: '#5E5CE6',
|
||||
background: '#F2F2F7',
|
||||
card: '#FFFFFF',
|
||||
text: '#000000',
|
||||
border: '#E5E5EA',
|
||||
notification: '#FF3B30',
|
||||
placeholder: '#8E8E93',
|
||||
accent: '#34C759',
|
||||
muted: '#C7C7CC',
|
||||
// Hover-States für Buttons und Menüpunkte
|
||||
buttonHover: 'rgba(255, 255, 255, 0.7)', // Neutral weiß, heller
|
||||
menuItemHover: 'rgba(255, 255, 255, 0.5)', // Neutral weiß, weniger intensiv
|
||||
cardHover: 'rgba(255, 255, 255, 0.3)', // Neutral weiß, subtil
|
||||
dangerHover: 'rgba(255, 255, 255, 0.5)', // Neutral weiß für Danger-Buttons
|
||||
},
|
||||
...DefaultTheme,
|
||||
colors: {
|
||||
...DefaultTheme.colors,
|
||||
primary: '#0A84FF',
|
||||
secondary: '#5E5CE6',
|
||||
background: '#F2F2F7',
|
||||
card: '#FFFFFF',
|
||||
text: '#000000',
|
||||
border: '#E5E5EA',
|
||||
notification: '#FF3B30',
|
||||
placeholder: '#8E8E93',
|
||||
accent: '#34C759',
|
||||
muted: '#C7C7CC',
|
||||
// Hover-States für Buttons und Menüpunkte
|
||||
buttonHover: 'rgba(255, 255, 255, 0.7)', // Neutral weiß, heller
|
||||
menuItemHover: 'rgba(255, 255, 255, 0.5)', // Neutral weiß, weniger intensiv
|
||||
cardHover: 'rgba(255, 255, 255, 0.3)', // Neutral weiß, subtil
|
||||
dangerHover: 'rgba(255, 255, 255, 0.5)', // Neutral weiß für Danger-Buttons
|
||||
},
|
||||
};
|
||||
|
||||
export const darkTheme = {
|
||||
...DarkTheme,
|
||||
colors: {
|
||||
...DarkTheme.colors,
|
||||
primary: '#0A84FF',
|
||||
secondary: '#5E5CE6',
|
||||
background: '#1C1C1E',
|
||||
card: '#2C2C2E',
|
||||
text: '#FFFFFF',
|
||||
border: '#38383A',
|
||||
notification: '#FF453A',
|
||||
placeholder: '#8E8E93',
|
||||
accent: '#30D158',
|
||||
muted: '#48484A',
|
||||
// Hover-States für Buttons und Menüpunkte
|
||||
buttonHover: 'rgba(255, 255, 255, 0.15)', // Neutral weiß, heller für Dark Mode
|
||||
menuItemHover: 'rgba(255, 255, 255, 0.1)', // Neutral weiß, dezent
|
||||
cardHover: 'rgba(255, 255, 255, 0.05)', // Neutral weiß, sehr dezent
|
||||
dangerHover: 'rgba(255, 255, 255, 0.15)', // Neutral weiß für Danger-Buttons
|
||||
},
|
||||
...DarkTheme,
|
||||
colors: {
|
||||
...DarkTheme.colors,
|
||||
primary: '#0A84FF',
|
||||
secondary: '#5E5CE6',
|
||||
background: '#1C1C1E',
|
||||
card: '#2C2C2E',
|
||||
text: '#FFFFFF',
|
||||
border: '#38383A',
|
||||
notification: '#FF453A',
|
||||
placeholder: '#8E8E93',
|
||||
accent: '#30D158',
|
||||
muted: '#48484A',
|
||||
// Hover-States für Buttons und Menüpunkte
|
||||
buttonHover: 'rgba(255, 255, 255, 0.15)', // Neutral weiß, heller für Dark Mode
|
||||
menuItemHover: 'rgba(255, 255, 255, 0.1)', // Neutral weiß, dezent
|
||||
cardHover: 'rgba(255, 255, 255, 0.05)', // Neutral weiß, sehr dezent
|
||||
dangerHover: 'rgba(255, 255, 255, 0.15)', // Neutral weiß für Danger-Buttons
|
||||
},
|
||||
};
|
||||
|
||||
// Tailwind-kompatible Farbpalette für die Verwendung in der tailwind.config.js
|
||||
export const tailwindColors = {
|
||||
// Light Mode Colors
|
||||
light: {
|
||||
primary: '#0A84FF',
|
||||
secondary: '#5E5CE6',
|
||||
background: '#F2F2F7',
|
||||
card: '#FFFFFF',
|
||||
text: '#000000',
|
||||
border: '#E5E5EA',
|
||||
notification: '#FF3B30',
|
||||
placeholder: '#8E8E93',
|
||||
accent: '#34C759',
|
||||
muted: '#C7C7CC',
|
||||
buttonHover: 'rgba(255, 255, 255, 0.7)',
|
||||
menuItemHover: 'rgba(255, 255, 255, 0.5)',
|
||||
cardHover: 'rgba(255, 255, 255, 0.3)',
|
||||
dangerHover: 'rgba(255, 255, 255, 0.5)',
|
||||
},
|
||||
// Dark Mode Colors
|
||||
dark: {
|
||||
primary: '#0A84FF',
|
||||
secondary: '#5E5CE6',
|
||||
background: '#1C1C1E',
|
||||
card: '#2C2C2E',
|
||||
text: '#FFFFFF',
|
||||
border: '#38383A',
|
||||
notification: '#FF453A',
|
||||
placeholder: '#8E8E93',
|
||||
accent: '#30D158',
|
||||
muted: '#48484A',
|
||||
buttonHover: 'rgba(255, 255, 255, 0.15)',
|
||||
menuItemHover: 'rgba(255, 255, 255, 0.1)',
|
||||
cardHover: 'rgba(255, 255, 255, 0.05)',
|
||||
dangerHover: 'rgba(255, 255, 255, 0.15)',
|
||||
},
|
||||
// Light Mode Colors
|
||||
light: {
|
||||
primary: '#0A84FF',
|
||||
secondary: '#5E5CE6',
|
||||
background: '#F2F2F7',
|
||||
card: '#FFFFFF',
|
||||
text: '#000000',
|
||||
border: '#E5E5EA',
|
||||
notification: '#FF3B30',
|
||||
placeholder: '#8E8E93',
|
||||
accent: '#34C759',
|
||||
muted: '#C7C7CC',
|
||||
buttonHover: 'rgba(255, 255, 255, 0.7)',
|
||||
menuItemHover: 'rgba(255, 255, 255, 0.5)',
|
||||
cardHover: 'rgba(255, 255, 255, 0.3)',
|
||||
dangerHover: 'rgba(255, 255, 255, 0.5)',
|
||||
},
|
||||
// Dark Mode Colors
|
||||
dark: {
|
||||
primary: '#0A84FF',
|
||||
secondary: '#5E5CE6',
|
||||
background: '#1C1C1E',
|
||||
card: '#2C2C2E',
|
||||
text: '#FFFFFF',
|
||||
border: '#38383A',
|
||||
notification: '#FF453A',
|
||||
placeholder: '#8E8E93',
|
||||
accent: '#30D158',
|
||||
muted: '#48484A',
|
||||
buttonHover: 'rgba(255, 255, 255, 0.15)',
|
||||
menuItemHover: 'rgba(255, 255, 255, 0.1)',
|
||||
cardHover: 'rgba(255, 255, 255, 0.05)',
|
||||
dangerHover: 'rgba(255, 255, 255, 0.15)',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,12 @@
|
|||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts",
|
||||
"nativewind-env.d.ts"
|
||||
]
|
||||
}
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts", "nativewind-env.d.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,42 +4,42 @@
|
|||
|
||||
// Typendefinition für ein KI-Modell
|
||||
export interface Model {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
parameters?: {
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
provider?: string;
|
||||
deployment?: string;
|
||||
endpoint?: string;
|
||||
api_version?: string;
|
||||
};
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
parameters?: {
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
provider?: string;
|
||||
deployment?: string;
|
||||
endpoint?: string;
|
||||
api_version?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Typendefinition für eine Nachricht
|
||||
export interface Message {
|
||||
id: string;
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
createdAt: string;
|
||||
conversationId: string;
|
||||
id: string;
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
createdAt: string;
|
||||
conversationId: string;
|
||||
}
|
||||
|
||||
// Typendefinition für eine Konversation
|
||||
export interface Conversation {
|
||||
id: string;
|
||||
title: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
userId: string;
|
||||
modelId: string;
|
||||
id: string;
|
||||
title: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
userId: string;
|
||||
modelId: string;
|
||||
}
|
||||
|
||||
// Typendefinition für Benutzer
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
id: string;
|
||||
email: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +1,72 @@
|
|||
// Model type definition
|
||||
export type Model = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
parameters?: Record<string, any>;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
parameters?: Record<string, any>;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
// Message types
|
||||
export type Message = {
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
sender: 'user' | 'assistant' | 'system';
|
||||
message_text: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
sender: 'user' | 'assistant' | 'system';
|
||||
message_text: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
// UI Message type used in components
|
||||
export type UIMessage = {
|
||||
id: string;
|
||||
text: string;
|
||||
sender: 'user' | 'ai';
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
text: string;
|
||||
sender: 'user' | 'ai';
|
||||
timestamp: Date;
|
||||
};
|
||||
|
||||
// Conversation type
|
||||
export type Conversation = {
|
||||
id: string; // UUID
|
||||
user_id: string; // UUID des Benutzers (auth.uid)
|
||||
model_id: string; // UUID
|
||||
template_id?: string; // UUID, optional
|
||||
conversation_mode: 'free' | 'guided' | 'template';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
id: string; // UUID
|
||||
user_id: string; // UUID des Benutzers (auth.uid)
|
||||
model_id: string; // UUID
|
||||
template_id?: string; // UUID, optional
|
||||
conversation_mode: 'free' | 'guided' | 'template';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
// Type for API requests to OpenAI/Azure
|
||||
export type ChatMessage = {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
};
|
||||
|
||||
// API response type from OpenAI/Azure
|
||||
export type ChatResponse = {
|
||||
id: string;
|
||||
choices: {
|
||||
content_filter_results?: any;
|
||||
finish_reason: string;
|
||||
index: number;
|
||||
logprobs: any;
|
||||
message?: {
|
||||
content: string;
|
||||
refusal?: any;
|
||||
role: string;
|
||||
};
|
||||
}[];
|
||||
created: number;
|
||||
model: string;
|
||||
object: string;
|
||||
prompt_filter_results?: any[];
|
||||
system_fingerprint?: string;
|
||||
usage: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
completion_tokens_details?: any;
|
||||
prompt_tokens_details?: any;
|
||||
};
|
||||
};
|
||||
id: string;
|
||||
choices: {
|
||||
content_filter_results?: any;
|
||||
finish_reason: string;
|
||||
index: number;
|
||||
logprobs: any;
|
||||
message?: {
|
||||
content: string;
|
||||
refusal?: any;
|
||||
role: string;
|
||||
};
|
||||
}[];
|
||||
created: number;
|
||||
model: string;
|
||||
object: string;
|
||||
prompt_filter_results?: any[];
|
||||
system_fingerprint?: string;
|
||||
usage: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
completion_tokens_details?: any;
|
||||
prompt_tokens_details?: any;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ export type { TokenUsage };
|
|||
|
||||
// Rückgabetyp für die Chat-Anfrage
|
||||
export type ChatRequestResult = {
|
||||
content: string;
|
||||
usage: TokenUsage;
|
||||
content: string;
|
||||
usage: TokenUsage;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -23,46 +23,41 @@ export type ChatRequestResult = {
|
|||
* @returns Die Antwort des Modells als Text mit Tokennutzung
|
||||
*/
|
||||
export async function sendChatRequest(
|
||||
messages: ChatMessage[],
|
||||
temperature: number = 0.7,
|
||||
maxTokens: number = 800,
|
||||
config: {
|
||||
endpoint?: string;
|
||||
apiKey?: string;
|
||||
deployment: string;
|
||||
apiVersion?: string;
|
||||
}
|
||||
messages: ChatMessage[],
|
||||
temperature: number = 0.7,
|
||||
maxTokens: number = 800,
|
||||
config: {
|
||||
endpoint?: string;
|
||||
apiKey?: string;
|
||||
deployment: string;
|
||||
apiVersion?: string;
|
||||
}
|
||||
): Promise<string | ChatRequestResult> {
|
||||
console.log('sendChatRequest via Backend:', {
|
||||
messagesCount: messages.length,
|
||||
deployment: config.deployment,
|
||||
temperature,
|
||||
maxTokens,
|
||||
});
|
||||
console.log('sendChatRequest via Backend:', {
|
||||
messagesCount: messages.length,
|
||||
deployment: config.deployment,
|
||||
temperature,
|
||||
maxTokens,
|
||||
});
|
||||
|
||||
try {
|
||||
// Map deployment name to model ID
|
||||
const modelId = getModelIdFromDeployment(config.deployment);
|
||||
try {
|
||||
// Map deployment name to model ID
|
||||
const modelId = getModelIdFromDeployment(config.deployment);
|
||||
|
||||
// Send request through backend
|
||||
const result = await sendChatCompletion(
|
||||
messages,
|
||||
modelId,
|
||||
temperature,
|
||||
maxTokens
|
||||
);
|
||||
// Send request through backend
|
||||
const result = await sendChatCompletion(messages, modelId, temperature, maxTokens);
|
||||
|
||||
return {
|
||||
content: result.content,
|
||||
usage: result.usage,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Backend API-Aufruf:', error);
|
||||
return {
|
||||
content: result.content,
|
||||
usage: result.usage,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Backend API-Aufruf:', error);
|
||||
|
||||
// Return user-friendly error message
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
return `Es konnte keine Antwort generiert werden. Bitte stelle sicher, dass das Backend läuft. Fehler: ${errorMessage}`;
|
||||
}
|
||||
// Return user-friendly error message
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
return `Es konnte keine Antwort generiert werden. Bitte stelle sicher, dass das Backend läuft. Fehler: ${errorMessage}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -70,18 +65,18 @@ export async function sendChatRequest(
|
|||
* This ensures backward compatibility with existing code
|
||||
*/
|
||||
function getModelIdFromDeployment(deployment: string): string {
|
||||
const deploymentToModelId: Record<string, string> = {
|
||||
'gpt-o3-mini-se': '550e8400-e29b-41d4-a716-446655440000',
|
||||
'gpt-4o-mini-se': '550e8400-e29b-41d4-a716-446655440004',
|
||||
'gpt-4o-se': '550e8400-e29b-41d4-a716-446655440005',
|
||||
};
|
||||
const deploymentToModelId: Record<string, string> = {
|
||||
'gpt-o3-mini-se': '550e8400-e29b-41d4-a716-446655440000',
|
||||
'gpt-4o-mini-se': '550e8400-e29b-41d4-a716-446655440004',
|
||||
'gpt-4o-se': '550e8400-e29b-41d4-a716-446655440005',
|
||||
};
|
||||
|
||||
const modelId = deploymentToModelId[deployment];
|
||||
const modelId = deploymentToModelId[deployment];
|
||||
|
||||
if (!modelId) {
|
||||
console.warn(`Unknown deployment: ${deployment}, using default model`);
|
||||
return '550e8400-e29b-41d4-a716-446655440000'; // Default to GPT-O3-Mini
|
||||
}
|
||||
if (!modelId) {
|
||||
console.warn(`Unknown deployment: ${deployment}, using default model`);
|
||||
return '550e8400-e29b-41d4-a716-446655440000'; // Default to GPT-O3-Mini
|
||||
}
|
||||
|
||||
return modelId;
|
||||
return modelId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,53 +10,53 @@ const BACKEND_URL = process.env.EXPO_PUBLIC_BACKEND_URL || 'http://localhost:300
|
|||
|
||||
// Token usage type
|
||||
export type TokenUsage = {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
|
||||
// Chat completion response
|
||||
export type ChatCompletionResponse = {
|
||||
content: string;
|
||||
usage: TokenUsage;
|
||||
content: string;
|
||||
usage: TokenUsage;
|
||||
};
|
||||
|
||||
// AI Model type from backend
|
||||
export type AIModel = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: {
|
||||
temperature: number;
|
||||
max_tokens: number;
|
||||
provider: string;
|
||||
deployment: string;
|
||||
endpoint: string;
|
||||
api_version: string;
|
||||
};
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: {
|
||||
temperature: number;
|
||||
max_tokens: number;
|
||||
provider: string;
|
||||
deployment: string;
|
||||
endpoint: string;
|
||||
api_version: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches available AI models from the backend
|
||||
*/
|
||||
export async function getAvailableModels(): Promise<AIModel[]> {
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/chat/models`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/chat/models`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch models: ${response.status}`);
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch models: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching models from backend:', error);
|
||||
throw error;
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching models from backend:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -64,69 +64,69 @@ export async function getAvailableModels(): Promise<AIModel[]> {
|
|||
* The backend securely handles the Azure OpenAI API call
|
||||
*/
|
||||
export async function sendChatCompletion(
|
||||
messages: ChatMessage[],
|
||||
modelId: string,
|
||||
temperature?: number,
|
||||
maxTokens?: number
|
||||
messages: ChatMessage[],
|
||||
modelId: string,
|
||||
temperature?: number,
|
||||
maxTokens?: number
|
||||
): Promise<ChatCompletionResponse> {
|
||||
console.log('Sending chat request to backend:', {
|
||||
messagesCount: messages.length,
|
||||
modelId,
|
||||
temperature,
|
||||
maxTokens,
|
||||
backendUrl: BACKEND_URL,
|
||||
});
|
||||
console.log('Sending chat request to backend:', {
|
||||
messagesCount: messages.length,
|
||||
modelId,
|
||||
temperature,
|
||||
maxTokens,
|
||||
backendUrl: BACKEND_URL,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: messages.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
})),
|
||||
modelId,
|
||||
temperature,
|
||||
maxTokens,
|
||||
}),
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: messages.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
})),
|
||||
modelId,
|
||||
temperature,
|
||||
maxTokens,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Backend API error:', response.status, errorText);
|
||||
throw new Error(`Backend API error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Backend API error:', response.status, errorText);
|
||||
throw new Error(`Backend API error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Backend response received:', {
|
||||
contentLength: data.content?.length,
|
||||
usage: data.usage,
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log('Backend response received:', {
|
||||
contentLength: data.content?.length,
|
||||
usage: data.usage,
|
||||
});
|
||||
|
||||
return {
|
||||
content: data.content,
|
||||
usage: data.usage,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error sending chat request to backend:', error);
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
content: data.content,
|
||||
usage: data.usage,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error sending chat request to backend:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check for the backend
|
||||
*/
|
||||
export async function checkBackendHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/health`, {
|
||||
method: 'GET',
|
||||
});
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Backend health check failed:', error);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/health`, {
|
||||
method: 'GET',
|
||||
});
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Backend health check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue