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:
Wuesteon 2025-11-27 18:33:16 +01:00
parent 0241f5554c
commit d36b321d9d
3952 changed files with 661498 additions and 739751 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"
}
}
}
}

View file

@ -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>
);
}

View file

@ -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 = `

View file

@ -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]`,
};

View file

@ -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>
);
}

View file

@ -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',
},
}
);
}

View file

@ -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' },
});
}
}

View file

@ -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',
},
});

View file

@ -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 }} />;
}

View file

@ -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',
},
});

View file

@ -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',
},
});

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,
},
});

View file

@ -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',
},
});

View file

@ -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,
},
});

View file

@ -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,
};
};

View file

@ -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"
}
}

View file

@ -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',
};

View file

@ -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,
},
});

View file

@ -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>
);
}

View file

@ -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;

View file

@ -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',
};

View file

@ -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;

View file

@ -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,
},
});

View file

@ -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,
},
});

View file

@ -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',
},
});

View file

@ -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`,
};

View file

@ -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;

View file

@ -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,
},
});

View file

@ -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,
},
});

View file

@ -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',
},
});

View file

@ -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%',
},
});

View file

@ -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',
},
});

View file

@ -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,
},
});

View file

@ -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,
},
});

View file

@ -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',
},
});

View file

@ -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,
},
});

View file

@ -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

View file

@ -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>
);
}

View file

@ -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": {}
}
}

View file

@ -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,
};
}

View file

@ -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.

View file

@ -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
}

View file

@ -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'],
};

View file

@ -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.

View file

@ -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>
);
}

View file

@ -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

View file

@ -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
```

View file

@ -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.)

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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

View file

@ -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

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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'}`;
}
}

View file

@ -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 [];
}
}

View file

@ -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;
}
}

View file

@ -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: [],
};

View file

@ -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();

View file

@ -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);

View file

@ -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>
);
}

View file

@ -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)',
},
};

View file

@ -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"]
}

View file

@ -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;
}

View file

@ -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;
};
};

View file

@ -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;
}

View file

@ -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;
}
}