mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 02:01:25 +02:00
style: auto-format codebase with Prettier
Applied formatting to 1487+ files using pnpm format:write - TypeScript/JavaScript files - Svelte components - Astro pages - JSON configs - Markdown docs 13 files still need manual review (Astro JSX comments)
This commit is contained in:
parent
0241f5554c
commit
d36b321d9d
3952 changed files with 661498 additions and 739751 deletions
|
|
@ -1,43 +1,43 @@
|
|||
{
|
||||
"name": "@chat/web",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/kit": "^2.43.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"svelte": "^5.39.5",
|
||||
"svelte-check": "^4.3.2",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chat/types": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
"@manacore/shared-i18n": "workspace:*",
|
||||
"@manacore/shared-icons": "workspace:*",
|
||||
"@manacore/shared-tailwind": "workspace:*",
|
||||
"@manacore/shared-theme": "workspace:*",
|
||||
"@manacore/shared-theme-ui": "workspace:*",
|
||||
"@manacore/shared-ui": "workspace:*",
|
||||
"@manacore/shared-utils": "workspace:*",
|
||||
"marked": "^17.0.0"
|
||||
}
|
||||
"name": "@chat/web",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/kit": "^2.43.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"svelte": "^5.39.5",
|
||||
"svelte-check": "^4.3.2",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chat/types": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
"@manacore/shared-i18n": "workspace:*",
|
||||
"@manacore/shared-icons": "workspace:*",
|
||||
"@manacore/shared-tailwind": "workspace:*",
|
||||
"@manacore/shared-theme": "workspace:*",
|
||||
"@manacore/shared-theme-ui": "workspace:*",
|
||||
"@manacore/shared-ui": "workspace:*",
|
||||
"@manacore/shared-utils": "workspace:*",
|
||||
"marked": "^17.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {}
|
||||
}
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
22
apps/chat/apps/web/src/app.d.ts
vendored
22
apps/chat/apps/web/src/app.d.ts
vendored
|
|
@ -2,17 +2,17 @@
|
|||
// for information about these interfaces
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
// Auth is now handled client-side via Mana Core Auth
|
||||
}
|
||||
interface PageData {
|
||||
pathname?: string;
|
||||
}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
// Auth is now handled client-side via Mana Core Auth
|
||||
}
|
||||
interface PageData {
|
||||
pathname?: string;
|
||||
}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
|
|
|||
|
|
@ -6,5 +6,5 @@
|
|||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return resolve(event);
|
||||
return resolve(event);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,82 +1,82 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
onSend: (message: string) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
interface Props {
|
||||
onSend: (message: string) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
let { onSend, disabled = false, placeholder = 'Nachricht eingeben...' }: Props = $props();
|
||||
let { onSend, disabled = false, placeholder = 'Nachricht eingeben...' }: Props = $props();
|
||||
|
||||
let inputValue = $state('');
|
||||
let textareaEl: HTMLTextAreaElement | undefined = $state();
|
||||
let inputValue = $state('');
|
||||
let textareaEl: HTMLTextAreaElement | undefined = $state();
|
||||
|
||||
function handleSubmit() {
|
||||
const trimmed = inputValue.trim();
|
||||
if (trimmed && !disabled) {
|
||||
onSend(trimmed);
|
||||
inputValue = '';
|
||||
// Reset textarea height
|
||||
if (textareaEl) {
|
||||
textareaEl.style.height = 'auto';
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleSubmit() {
|
||||
const trimmed = inputValue.trim();
|
||||
if (trimmed && !disabled) {
|
||||
onSend(trimmed);
|
||||
inputValue = '';
|
||||
// Reset textarea height
|
||||
if (textareaEl) {
|
||||
textareaEl.style.height = 'auto';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
function handleInput() {
|
||||
// Auto-resize textarea
|
||||
if (textareaEl) {
|
||||
textareaEl.style.height = 'auto';
|
||||
textareaEl.style.height = Math.min(textareaEl.scrollHeight, 200) + 'px';
|
||||
}
|
||||
}
|
||||
function handleInput() {
|
||||
// Auto-resize textarea
|
||||
if (textareaEl) {
|
||||
textareaEl.style.height = 'auto';
|
||||
textareaEl.style.height = Math.min(textareaEl.scrollHeight, 200) + 'px';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-4">
|
||||
<div class="flex items-end gap-3 max-w-4xl mx-auto">
|
||||
<div class="flex-1 relative">
|
||||
<textarea
|
||||
bind:this={textareaEl}
|
||||
bind:value={inputValue}
|
||||
onkeydown={handleKeyDown}
|
||||
oninput={handleInput}
|
||||
{placeholder}
|
||||
{disabled}
|
||||
rows="1"
|
||||
class="w-full resize-none rounded-xl border border-gray-300 dark:border-gray-600
|
||||
<div class="flex items-end gap-3 max-w-4xl mx-auto">
|
||||
<div class="flex-1 relative">
|
||||
<textarea
|
||||
bind:this={textareaEl}
|
||||
bind:value={inputValue}
|
||||
onkeydown={handleKeyDown}
|
||||
oninput={handleInput}
|
||||
{placeholder}
|
||||
{disabled}
|
||||
rows="1"
|
||||
class="w-full resize-none rounded-xl border border-gray-300 dark:border-gray-600
|
||||
bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100
|
||||
px-4 py-3 text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
placeholder:text-gray-500 dark:placeholder:text-gray-400"
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
onclick={handleSubmit}
|
||||
disabled={disabled || !inputValue.trim()}
|
||||
aria-label="Nachricht senden"
|
||||
class="flex-shrink-0 p-3 rounded-xl bg-blue-600 text-white
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
onclick={handleSubmit}
|
||||
disabled={disabled || !inputValue.trim()}
|
||||
aria-label="Nachricht senden"
|
||||
class="flex-shrink-0 p-3 rounded-xl bg-blue-600 text-white
|
||||
hover:bg-blue-700 active:bg-blue-800
|
||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600
|
||||
transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 text-center mt-2">
|
||||
Enter zum Senden, Shift+Enter für neue Zeile
|
||||
</p>
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 text-center mt-2">
|
||||
Enter zum Senden, Shift+Enter für neue Zeile
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,87 +1,89 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import type { Conversation } from '@chat/types';
|
||||
import { page } from '$app/stores';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import type { Conversation } from '@chat/types';
|
||||
|
||||
interface Props {
|
||||
conversations: Conversation[];
|
||||
isLoading?: boolean;
|
||||
}
|
||||
interface Props {
|
||||
conversations: Conversation[];
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
let { conversations, isLoading = false }: Props = $props();
|
||||
let { conversations, isLoading = false }: Props = $props();
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
} else if (diffDays === 1) {
|
||||
return 'Gestern';
|
||||
} else if (diffDays < 7) {
|
||||
return date.toLocaleDateString('de-DE', { weekday: 'short' });
|
||||
} else {
|
||||
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
|
||||
}
|
||||
}
|
||||
if (diffDays === 0) {
|
||||
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
} else if (diffDays === 1) {
|
||||
return 'Gestern';
|
||||
} else if (diffDays < 7) {
|
||||
return date.toLocaleDateString('de-DE', { weekday: 'short' });
|
||||
} else {
|
||||
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
|
||||
}
|
||||
}
|
||||
|
||||
function truncateTitle(title: string, maxLength: number = 30): string {
|
||||
if (title.length <= maxLength) return title;
|
||||
return title.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
function truncateTitle(title: string, maxLength: number = 30): string {
|
||||
if (title.length <= maxLength) return title;
|
||||
return title.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- New Chat Button -->
|
||||
<div class="p-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<a
|
||||
href="/chat"
|
||||
class="flex items-center justify-center gap-2 w-full px-4 py-2.5
|
||||
<!-- New Chat Button -->
|
||||
<div class="p-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<a
|
||||
href="/chat"
|
||||
class="flex items-center justify-center gap-2 w-full px-4 py-2.5
|
||||
bg-blue-600 hover:bg-blue-700 text-white rounded-lg
|
||||
font-medium transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Neuer Chat
|
||||
</a>
|
||||
</div>
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Neuer Chat
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Conversation List -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
{#if isLoading}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="animate-spin w-6 h-6 border-2 border-blue-500 border-r-transparent rounded-full"></div>
|
||||
</div>
|
||||
{:else if conversations.length === 0}
|
||||
<div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<p class="text-sm">Keine Konversationen</p>
|
||||
<p class="text-xs mt-1">Starte einen neuen Chat</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="py-2">
|
||||
{#each conversations as conv (conv.id)}
|
||||
{@const isActive = $page.params.id === conv.id}
|
||||
<a
|
||||
href="/chat/{conv.id}"
|
||||
class="block px-3 py-2 mx-2 rounded-lg transition-colors
|
||||
<!-- Conversation List -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
{#if isLoading}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div
|
||||
class="animate-spin w-6 h-6 border-2 border-blue-500 border-r-transparent rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
{:else if conversations.length === 0}
|
||||
<div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<p class="text-sm">Keine Konversationen</p>
|
||||
<p class="text-xs mt-1">Starte einen neuen Chat</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="py-2">
|
||||
{#each conversations as conv (conv.id)}
|
||||
{@const isActive = $page.params.id === conv.id}
|
||||
<a
|
||||
href="/chat/{conv.id}"
|
||||
class="block px-3 py-2 mx-2 rounded-lg transition-colors
|
||||
{isActive
|
||||
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300'}"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-sm font-medium truncate">
|
||||
{truncateTitle(conv.title || 'Neue Konversation')}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-500 flex-shrink-0">
|
||||
{formatDate(conv.updated_at || conv.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300'}"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-sm font-medium truncate">
|
||||
{truncateTitle(conv.title || 'Neue Konversation')}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-500 flex-shrink-0">
|
||||
{formatDate(conv.updated_at || conv.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,52 +1,46 @@
|
|||
<script lang="ts">
|
||||
import { marked } from 'marked';
|
||||
import type { Message } from '@chat/types';
|
||||
import { marked } from 'marked';
|
||||
import type { Message } from '@chat/types';
|
||||
|
||||
interface Props {
|
||||
message: Message;
|
||||
}
|
||||
interface Props {
|
||||
message: Message;
|
||||
}
|
||||
|
||||
let { message }: Props = $props();
|
||||
let { message }: Props = $props();
|
||||
|
||||
const isUser = $derived(message.sender === 'user');
|
||||
const isUser = $derived(message.sender === 'user');
|
||||
|
||||
// Configure marked for safe rendering
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
// Configure marked for safe rendering
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
|
||||
const htmlContent = $derived(
|
||||
isUser ? message.message_text : marked.parse(message.message_text)
|
||||
);
|
||||
const htmlContent = $derived(isUser ? message.message_text : marked.parse(message.message_text));
|
||||
|
||||
const formattedTime = $derived(
|
||||
new Date(message.created_at).toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
);
|
||||
const formattedTime = $derived(
|
||||
new Date(message.created_at).toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex {isUser ? 'justify-end' : 'justify-start'} mb-4">
|
||||
<div
|
||||
class="max-w-[80%] rounded-2xl px-4 py-3 {isUser
|
||||
? 'bg-blue-600 text-white rounded-br-md'
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-bl-md'}"
|
||||
>
|
||||
{#if isUser}
|
||||
<p class="whitespace-pre-wrap">{message.message_text}</p>
|
||||
{:else}
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
{@html htmlContent}
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="text-xs mt-1 {isUser
|
||||
? 'text-blue-200'
|
||||
: 'text-gray-500 dark:text-gray-400'}"
|
||||
>
|
||||
{formattedTime}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="max-w-[80%] rounded-2xl px-4 py-3 {isUser
|
||||
? 'bg-blue-600 text-white rounded-br-md'
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-bl-md'}"
|
||||
>
|
||||
{#if isUser}
|
||||
<p class="whitespace-pre-wrap">{message.message_text}</p>
|
||||
{:else}
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
{@html htmlContent}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-xs mt-1 {isUser ? 'text-blue-200' : 'text-gray-500 dark:text-gray-400'}">
|
||||
{formattedTime}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,64 +1,56 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import type { Message } from '@chat/types';
|
||||
import MessageBubble from './MessageBubble.svelte';
|
||||
import TypingIndicator from './TypingIndicator.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Message } from '@chat/types';
|
||||
import MessageBubble from './MessageBubble.svelte';
|
||||
import TypingIndicator from './TypingIndicator.svelte';
|
||||
|
||||
interface Props {
|
||||
messages: Message[];
|
||||
isTyping?: boolean;
|
||||
}
|
||||
interface Props {
|
||||
messages: Message[];
|
||||
isTyping?: boolean;
|
||||
}
|
||||
|
||||
let { messages, isTyping = false }: Props = $props();
|
||||
let { messages, isTyping = false }: Props = $props();
|
||||
|
||||
let containerEl: HTMLDivElement | undefined = $state();
|
||||
let containerEl: HTMLDivElement | undefined = $state();
|
||||
|
||||
// Auto-scroll to bottom when messages change
|
||||
$effect(() => {
|
||||
if (messages.length > 0 && containerEl) {
|
||||
scrollToBottom();
|
||||
}
|
||||
});
|
||||
// Auto-scroll to bottom when messages change
|
||||
$effect(() => {
|
||||
if (messages.length > 0 && containerEl) {
|
||||
scrollToBottom();
|
||||
}
|
||||
});
|
||||
|
||||
function scrollToBottom() {
|
||||
if (containerEl) {
|
||||
containerEl.scrollTop = containerEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
function scrollToBottom() {
|
||||
if (containerEl) {
|
||||
containerEl.scrollTop = containerEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
onMount(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={containerEl}
|
||||
class="flex-1 overflow-y-auto px-4 py-6"
|
||||
>
|
||||
{#if messages.length === 0}
|
||||
<div class="flex flex-col items-center justify-center h-full text-gray-500 dark:text-gray-400">
|
||||
<svg
|
||||
class="w-16 h-16 mb-4 opacity-50"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-lg font-medium">Keine Nachrichten</p>
|
||||
<p class="text-sm">Starte eine Konversation!</p>
|
||||
</div>
|
||||
{:else}
|
||||
{#each messages as message (message.id)}
|
||||
<MessageBubble {message} />
|
||||
{/each}
|
||||
{#if isTyping}
|
||||
<TypingIndicator />
|
||||
{/if}
|
||||
{/if}
|
||||
<div bind:this={containerEl} class="flex-1 overflow-y-auto px-4 py-6">
|
||||
{#if messages.length === 0}
|
||||
<div class="flex flex-col items-center justify-center h-full text-gray-500 dark:text-gray-400">
|
||||
<svg class="w-16 h-16 mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-lg font-medium">Keine Nachrichten</p>
|
||||
<p class="text-sm">Starte eine Konversation!</p>
|
||||
</div>
|
||||
{:else}
|
||||
{#each messages as message (message.id)}
|
||||
<MessageBubble {message} />
|
||||
{/each}
|
||||
{#if isTyping}
|
||||
<TypingIndicator />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,55 +1,43 @@
|
|||
<script lang="ts">
|
||||
import type { AIModel } from '@chat/types';
|
||||
import type { AIModel } from '@chat/types';
|
||||
|
||||
interface Props {
|
||||
models: AIModel[];
|
||||
selectedModelId: string;
|
||||
onSelect: (modelId: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
interface Props {
|
||||
models: AIModel[];
|
||||
selectedModelId: string;
|
||||
onSelect: (modelId: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
let { models, selectedModelId, onSelect, disabled = false }: Props = $props();
|
||||
let { models, selectedModelId, onSelect, disabled = false }: Props = $props();
|
||||
|
||||
function handleChange(e: Event) {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
onSelect(target.value);
|
||||
}
|
||||
function handleChange(e: Event) {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
onSelect(target.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<select
|
||||
value={selectedModelId}
|
||||
onchange={handleChange}
|
||||
{disabled}
|
||||
class="appearance-none bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100
|
||||
<select
|
||||
value={selectedModelId}
|
||||
onchange={handleChange}
|
||||
{disabled}
|
||||
class="appearance-none bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100
|
||||
text-sm rounded-lg px-3 py-2 pr-8 border border-gray-200 dark:border-gray-700
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
cursor-pointer min-w-[160px]"
|
||||
>
|
||||
{#if models.length === 0}
|
||||
<option value="">Laden...</option>
|
||||
{:else}
|
||||
{#each models as model (model.id)}
|
||||
<option value={model.id}>{model.name}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
<div
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-gray-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
>
|
||||
{#if models.length === 0}
|
||||
<option value="">Laden...</option>
|
||||
{:else}
|
||||
{#each models as model (model.id)}
|
||||
<option value={model.id}>{model.name}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,41 +1,39 @@
|
|||
<script lang="ts">
|
||||
// Typing indicator with animated dots
|
||||
// Typing indicator with animated dots
|
||||
</script>
|
||||
|
||||
<div class="flex justify-start mb-4">
|
||||
<div
|
||||
class="bg-gray-100 dark:bg-gray-800 rounded-2xl rounded-bl-md px-4 py-3"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"
|
||||
style="animation-delay: 0ms"
|
||||
></div>
|
||||
<div
|
||||
class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"
|
||||
style="animation-delay: 150ms"
|
||||
></div>
|
||||
<div
|
||||
class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"
|
||||
style="animation-delay: 300ms"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-100 dark:bg-gray-800 rounded-2xl rounded-bl-md px-4 py-3">
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"
|
||||
style="animation-delay: 0ms"
|
||||
></div>
|
||||
<div
|
||||
class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"
|
||||
style="animation-delay: 150ms"
|
||||
></div>
|
||||
<div
|
||||
class="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce"
|
||||
style="animation-delay: 300ms"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
60%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
30% {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
60%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
30% {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-bounce {
|
||||
animation: bounce 1s infinite;
|
||||
}
|
||||
.animate-bounce {
|
||||
animation: bounce 1s infinite;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,162 +1,174 @@
|
|||
<script lang="ts">
|
||||
import type { Space } from '@chat/types';
|
||||
import type { Space } from '@chat/types';
|
||||
|
||||
interface Props {
|
||||
space: Space;
|
||||
isOwner: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
onEdit: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onLeave: (id: string) => void;
|
||||
}
|
||||
interface Props {
|
||||
space: Space;
|
||||
isOwner: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
onEdit: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onLeave: (id: string) => void;
|
||||
}
|
||||
|
||||
let { space, isOwner, onSelect, onEdit, onDelete, onLeave }: Props = $props();
|
||||
let showMenu = $state(false);
|
||||
let { space, isOwner, onSelect, onEdit, onDelete, onLeave }: Props = $props();
|
||||
let showMenu = $state(false);
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
function handleMenuClick(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
showMenu = !showMenu;
|
||||
}
|
||||
function handleMenuClick(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
showMenu = !showMenu;
|
||||
}
|
||||
|
||||
function handleAction(action: () => void) {
|
||||
showMenu = false;
|
||||
action();
|
||||
}
|
||||
function handleAction(action: () => void) {
|
||||
showMenu = false;
|
||||
action();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onclick={() => (showMenu = false)} />
|
||||
|
||||
<div
|
||||
class="group relative bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700
|
||||
class="group relative bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700
|
||||
shadow-sm hover:shadow-md transition-all cursor-pointer"
|
||||
onclick={() => onSelect(space.id)}
|
||||
onkeydown={(e) => e.key === 'Enter' && onSelect(space.id)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => onSelect(space.id)}
|
||||
onkeydown={(e) => e.key === 'Enter' && onSelect(space.id)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="p-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<svg class="w-5 h-5 text-blue-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-white truncate">
|
||||
{space.name}
|
||||
</h3>
|
||||
{#if isOwner}
|
||||
<span class="px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded">
|
||||
Besitzer
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<svg
|
||||
class="w-5 h-5 text-blue-500 flex-shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-white truncate">
|
||||
{space.name}
|
||||
</h3>
|
||||
{#if isOwner}
|
||||
<span
|
||||
class="px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded"
|
||||
>
|
||||
Besitzer
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if space.description}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mb-2">
|
||||
{space.description}
|
||||
</p>
|
||||
{/if}
|
||||
{#if space.description}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mb-2">
|
||||
{space.description}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<p class="text-xs text-gray-500 dark:text-gray-500">
|
||||
Erstellt: {formatDate(space.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-500">
|
||||
Erstellt: {formatDate(space.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Options Menu -->
|
||||
<div class="relative">
|
||||
<button
|
||||
onclick={handleMenuClick}
|
||||
class="p-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300
|
||||
<!-- Options Menu -->
|
||||
<div class="relative">
|
||||
<button
|
||||
onclick={handleMenuClick}
|
||||
class="p-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300
|
||||
hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
aria-label="Optionen"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
aria-label="Optionen"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{#if showMenu}
|
||||
<div
|
||||
class="absolute right-0 top-full mt-1 py-1 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg
|
||||
{#if showMenu}
|
||||
<div
|
||||
class="absolute right-0 top-full mt-1 py-1 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg
|
||||
border border-gray-200 dark:border-gray-700 z-10"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={() => {}}
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
>
|
||||
{#if isOwner}
|
||||
<button
|
||||
onclick={() => handleAction(() => onEdit(space.id))}
|
||||
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-300
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={() => {}}
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
>
|
||||
{#if isOwner}
|
||||
<button
|
||||
onclick={() => handleAction(() => onEdit(space.id))}
|
||||
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-300
|
||||
hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
role="menuitem"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
Einstellungen
|
||||
</button>
|
||||
<button
|
||||
onclick={() => handleAction(() => onDelete(space.id))}
|
||||
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-600 dark:text-red-400
|
||||
role="menuitem"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
Einstellungen
|
||||
</button>
|
||||
<button
|
||||
onclick={() => handleAction(() => onDelete(space.id))}
|
||||
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-600 dark:text-red-400
|
||||
hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||
role="menuitem"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
Löschen
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
onclick={() => handleAction(() => onLeave(space.id))}
|
||||
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-600 dark:text-red-400
|
||||
role="menuitem"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
Löschen
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
onclick={() => handleAction(() => onLeave(space.id))}
|
||||
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-600 dark:text-red-400
|
||||
hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||
role="menuitem"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
Verlassen
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
role="menuitem"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
Verlassen
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,102 +1,111 @@
|
|||
<script lang="ts">
|
||||
import type { Space } from '@chat/types';
|
||||
import type { Space } from '@chat/types';
|
||||
|
||||
interface Props {
|
||||
space?: Space;
|
||||
onSubmit: (data: { name: string; description?: string }) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
interface Props {
|
||||
space?: Space;
|
||||
onSubmit: (data: { name: string; description?: string }) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
let { space, onSubmit, onCancel }: Props = $props();
|
||||
let { space, onSubmit, onCancel }: Props = $props();
|
||||
|
||||
let name = $state(space?.name ?? '');
|
||||
let description = $state(space?.description ?? '');
|
||||
let errors = $state<{ name?: string }>({});
|
||||
let name = $state(space?.name ?? '');
|
||||
let description = $state(space?.description ?? '');
|
||||
let errors = $state<{ name?: string }>({});
|
||||
|
||||
const isEditMode = !!space?.id;
|
||||
const isEditMode = !!space?.id;
|
||||
|
||||
function validateForm(): boolean {
|
||||
const newErrors: { name?: string } = {};
|
||||
function validateForm(): boolean {
|
||||
const newErrors: { name?: string } = {};
|
||||
|
||||
if (!name.trim()) {
|
||||
newErrors.name = 'Bitte gib einen Namen ein.';
|
||||
}
|
||||
if (!name.trim()) {
|
||||
newErrors.name = 'Bitte gib einen Namen ein.';
|
||||
}
|
||||
|
||||
errors = newErrors;
|
||||
return Object.keys(newErrors).length === 0;
|
||||
}
|
||||
errors = newErrors;
|
||||
return Object.keys(newErrors).length === 0;
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (!validateForm()) return;
|
||||
function handleSubmit() {
|
||||
if (!validateForm()) return;
|
||||
|
||||
onSubmit({
|
||||
name: name.trim(),
|
||||
description: description.trim() || undefined,
|
||||
});
|
||||
}
|
||||
onSubmit({
|
||||
name: name.trim(),
|
||||
description: description.trim() || undefined,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bg-white dark:bg-gray-900 p-6 rounded-xl max-w-lg mx-auto">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
{isEditMode ? 'Space bearbeiten' : 'Neuen Space erstellen'}
|
||||
</h2>
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
{isEditMode ? 'Space bearbeiten' : 'Neuen Space erstellen'}
|
||||
</h2>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-5">
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
bind:value={name}
|
||||
maxlength={100}
|
||||
placeholder="Name des Spaces"
|
||||
class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-800
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
class="space-y-5"
|
||||
>
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
bind:value={name}
|
||||
maxlength={100}
|
||||
placeholder="Name des Spaces"
|
||||
class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-800
|
||||
text-gray-900 dark:text-white placeholder-gray-500
|
||||
{errors.name ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
{#if errors.name}
|
||||
<p class="mt-1 text-sm text-red-500">{errors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
/>
|
||||
{#if errors.name}
|
||||
<p class="mt-1 text-sm text-red-500">{errors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Beschreibung (optional)
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
bind:value={description}
|
||||
maxlength={500}
|
||||
rows={3}
|
||||
placeholder="Worum geht es in diesem Space?"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label
|
||||
for="description"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Beschreibung (optional)
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
bind:value={description}
|
||||
maxlength={500}
|
||||
rows={3}
|
||||
placeholder="Worum geht es in diesem Space?"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onclick={onCancel}
|
||||
class="flex-1 px-4 py-2.5 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300
|
||||
<!-- Buttons -->
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onclick={onCancel}
|
||||
class="flex-1 px-4 py-2.5 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300
|
||||
rounded-lg font-medium hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="flex-1 px-4 py-2.5 bg-blue-600 text-white rounded-lg font-medium
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="flex-1 px-4 py-2.5 bg-blue-600 text-white rounded-lg font-medium
|
||||
hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
{isEditMode ? 'Speichern' : 'Erstellen'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
>
|
||||
{isEditMode ? 'Speichern' : 'Erstellen'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,114 +1,116 @@
|
|||
<script lang="ts">
|
||||
import type { Template } from '@chat/types';
|
||||
import type { Template } from '@chat/types';
|
||||
|
||||
interface Props {
|
||||
template: Template;
|
||||
onUse: (id: string) => void;
|
||||
onEdit: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onSetDefault: (id: string) => void;
|
||||
}
|
||||
interface Props {
|
||||
template: Template;
|
||||
onUse: (id: string) => void;
|
||||
onEdit: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onSetDefault: (id: string) => void;
|
||||
}
|
||||
|
||||
let { template, onUse, onEdit, onDelete, onSetDefault }: Props = $props();
|
||||
let { template, onUse, onEdit, onDelete, onSetDefault }: Props = $props();
|
||||
|
||||
function truncatePrompt(text: string, maxLength: number = 80): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
function truncatePrompt(text: string, maxLength: number = 80): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="group relative flex rounded-xl overflow-hidden bg-white dark:bg-gray-800 shadow-sm hover:shadow-md transition-all
|
||||
{template.is_default ? 'ring-2 ring-blue-500' : 'border border-gray-200 dark:border-gray-700'}"
|
||||
class="group relative flex rounded-xl overflow-hidden bg-white dark:bg-gray-800 shadow-sm hover:shadow-md transition-all
|
||||
{template.is_default
|
||||
? 'ring-2 ring-blue-500'
|
||||
: 'border border-gray-200 dark:border-gray-700'}"
|
||||
>
|
||||
<!-- Color Indicator -->
|
||||
<div class="w-2 flex-shrink-0" style="background-color: {template.color}"></div>
|
||||
<!-- Color Indicator -->
|
||||
<div class="w-2 flex-shrink-0" style="background-color: {template.color}"></div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 p-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-white truncate">
|
||||
{template.name}
|
||||
</h3>
|
||||
{#if template.is_default}
|
||||
<span class="px-2 py-0.5 text-xs font-medium bg-blue-500 text-white rounded">
|
||||
Standard
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div class="flex-1 p-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-white truncate">
|
||||
{template.name}
|
||||
</h3>
|
||||
{#if template.is_default}
|
||||
<span class="px-2 py-0.5 text-xs font-medium bg-blue-500 text-white rounded">
|
||||
Standard
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if template.description}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mb-2">
|
||||
{template.description}
|
||||
</p>
|
||||
{/if}
|
||||
{#if template.description}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mb-2">
|
||||
{template.description}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<p class="text-xs text-gray-500 dark:text-gray-500 italic line-clamp-2">
|
||||
{truncatePrompt(template.system_prompt)}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-500 italic line-clamp-2">
|
||||
{truncatePrompt(template.system_prompt)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{#if !template.is_default}
|
||||
<button
|
||||
onclick={() => onSetDefault(template.id)}
|
||||
class="p-1.5 text-gray-500 hover:text-yellow-500 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
title="Als Standard setzen"
|
||||
aria-label="Als Standard setzen"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
onclick={() => onEdit(template.id)}
|
||||
class="p-1.5 text-gray-500 hover:text-blue-500 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
title="Bearbeiten"
|
||||
aria-label="Bearbeiten"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onclick={() => onDelete(template.id)}
|
||||
class="p-1.5 text-gray-500 hover:text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
title="Löschen"
|
||||
aria-label="Löschen"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{#if !template.is_default}
|
||||
<button
|
||||
onclick={() => onSetDefault(template.id)}
|
||||
class="p-1.5 text-gray-500 hover:text-yellow-500 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
title="Als Standard setzen"
|
||||
aria-label="Als Standard setzen"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
onclick={() => onEdit(template.id)}
|
||||
class="p-1.5 text-gray-500 hover:text-blue-500 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
title="Bearbeiten"
|
||||
aria-label="Bearbeiten"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onclick={() => onDelete(template.id)}
|
||||
class="p-1.5 text-gray-500 hover:text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
title="Löschen"
|
||||
aria-label="Löschen"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Use Button -->
|
||||
<button
|
||||
onclick={() => onUse(template.id)}
|
||||
class="mt-3 w-full py-2 px-3 text-sm font-medium text-white rounded-lg transition-colors"
|
||||
style="background-color: {template.color}"
|
||||
>
|
||||
Chat starten
|
||||
</button>
|
||||
</div>
|
||||
<!-- Use Button -->
|
||||
<button
|
||||
onclick={() => onUse(template.id)}
|
||||
class="mt-3 w-full py-2 px-3 text-sm font-medium text-white rounded-lg transition-colors"
|
||||
style="background-color: {template.color}"
|
||||
>
|
||||
Chat starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,265 +1,295 @@
|
|||
<script lang="ts">
|
||||
import type { Template, AIModel } from '@chat/types';
|
||||
import { chatService } from '$lib/services/chat';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Template, AIModel } from '@chat/types';
|
||||
import { chatService } from '$lib/services/chat';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
template?: Template;
|
||||
onSubmit: (data: Partial<Template>) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
interface Props {
|
||||
template?: Template;
|
||||
onSubmit: (data: Partial<Template>) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
let { template, onSubmit, onCancel }: Props = $props();
|
||||
let { template, onSubmit, onCancel }: Props = $props();
|
||||
|
||||
// Available colors
|
||||
const TEMPLATE_COLORS = [
|
||||
'#0A84FF', // Blue
|
||||
'#32D74B', // Green
|
||||
'#FF375F', // Red
|
||||
'#FF9F0A', // Orange
|
||||
'#5E5CE6', // Purple
|
||||
'#BF5AF2', // Pink
|
||||
'#64D2FF', // Light Blue
|
||||
'#30D158', // Green 2
|
||||
'#FF453A', // Red 2
|
||||
];
|
||||
// Available colors
|
||||
const TEMPLATE_COLORS = [
|
||||
'#0A84FF', // Blue
|
||||
'#32D74B', // Green
|
||||
'#FF375F', // Red
|
||||
'#FF9F0A', // Orange
|
||||
'#5E5CE6', // Purple
|
||||
'#BF5AF2', // Pink
|
||||
'#64D2FF', // Light Blue
|
||||
'#30D158', // Green 2
|
||||
'#FF453A', // Red 2
|
||||
];
|
||||
|
||||
// Form state
|
||||
let name = $state(template?.name ?? '');
|
||||
let description = $state(template?.description ?? '');
|
||||
let systemPrompt = $state(template?.system_prompt ?? '');
|
||||
let initialQuestion = $state(template?.initial_question ?? '');
|
||||
let selectedColor = $state(template?.color ?? TEMPLATE_COLORS[0]);
|
||||
let selectedModelId = $state(template?.model_id ?? '');
|
||||
let documentMode = $state(template?.document_mode ?? false);
|
||||
// Form state
|
||||
let name = $state(template?.name ?? '');
|
||||
let description = $state(template?.description ?? '');
|
||||
let systemPrompt = $state(template?.system_prompt ?? '');
|
||||
let initialQuestion = $state(template?.initial_question ?? '');
|
||||
let selectedColor = $state(template?.color ?? TEMPLATE_COLORS[0]);
|
||||
let selectedModelId = $state(template?.model_id ?? '');
|
||||
let documentMode = $state(template?.document_mode ?? false);
|
||||
|
||||
// Models
|
||||
let models = $state<AIModel[]>([]);
|
||||
// Models
|
||||
let models = $state<AIModel[]>([]);
|
||||
|
||||
// Validation
|
||||
let errors = $state<{ name?: string; systemPrompt?: string }>({});
|
||||
// Validation
|
||||
let errors = $state<{ name?: string; systemPrompt?: string }>({});
|
||||
|
||||
const isEditMode = !!template?.id;
|
||||
const isEditMode = !!template?.id;
|
||||
|
||||
onMount(async () => {
|
||||
models = await chatService.getModels();
|
||||
});
|
||||
onMount(async () => {
|
||||
models = await chatService.getModels();
|
||||
});
|
||||
|
||||
function validateForm(): boolean {
|
||||
const newErrors: { name?: string; systemPrompt?: string } = {};
|
||||
function validateForm(): boolean {
|
||||
const newErrors: { name?: string; systemPrompt?: string } = {};
|
||||
|
||||
if (!name.trim()) {
|
||||
newErrors.name = 'Bitte gib einen Namen ein.';
|
||||
}
|
||||
if (!name.trim()) {
|
||||
newErrors.name = 'Bitte gib einen Namen ein.';
|
||||
}
|
||||
|
||||
if (!systemPrompt.trim()) {
|
||||
newErrors.systemPrompt = 'Der System-Prompt darf nicht leer sein.';
|
||||
}
|
||||
if (!systemPrompt.trim()) {
|
||||
newErrors.systemPrompt = 'Der System-Prompt darf nicht leer sein.';
|
||||
}
|
||||
|
||||
errors = newErrors;
|
||||
return Object.keys(newErrors).length === 0;
|
||||
}
|
||||
errors = newErrors;
|
||||
return Object.keys(newErrors).length === 0;
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (!validateForm()) return;
|
||||
function handleSubmit() {
|
||||
if (!validateForm()) return;
|
||||
|
||||
onSubmit({
|
||||
id: template?.id,
|
||||
name,
|
||||
description: description.trim() || null,
|
||||
system_prompt: systemPrompt,
|
||||
initial_question: initialQuestion.trim() || null,
|
||||
color: selectedColor,
|
||||
model_id: selectedModelId || null,
|
||||
document_mode: documentMode,
|
||||
});
|
||||
}
|
||||
onSubmit({
|
||||
id: template?.id,
|
||||
name,
|
||||
description: description.trim() || null,
|
||||
system_prompt: systemPrompt,
|
||||
initial_question: initialQuestion.trim() || null,
|
||||
color: selectedColor,
|
||||
model_id: selectedModelId || null,
|
||||
document_mode: documentMode,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bg-white dark:bg-gray-900 p-6 rounded-xl max-w-2xl mx-auto">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
{isEditMode ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen'}
|
||||
</h2>
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
{isEditMode ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen'}
|
||||
</h2>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-5">
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
bind:value={name}
|
||||
maxlength={50}
|
||||
placeholder="Name der Vorlage"
|
||||
class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-800
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
class="space-y-5"
|
||||
>
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
bind:value={name}
|
||||
maxlength={50}
|
||||
placeholder="Name der Vorlage"
|
||||
class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-800
|
||||
text-gray-900 dark:text-white placeholder-gray-500
|
||||
{errors.name ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
{#if errors.name}
|
||||
<p class="mt-1 text-sm text-red-500">{errors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
/>
|
||||
{#if errors.name}
|
||||
<p class="mt-1 text-sm text-red-500">{errors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Beschreibung (optional)
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
bind:value={description}
|
||||
maxlength={200}
|
||||
rows={2}
|
||||
placeholder="Kurze Beschreibung dieser Vorlage"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label
|
||||
for="description"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Beschreibung (optional)
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
bind:value={description}
|
||||
maxlength={200}
|
||||
rows={2}
|
||||
placeholder="Kurze Beschreibung dieser Vorlage"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- System Prompt -->
|
||||
<div>
|
||||
<label for="systemPrompt" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
System-Prompt *
|
||||
</label>
|
||||
<textarea
|
||||
id="systemPrompt"
|
||||
bind:value={systemPrompt}
|
||||
rows={5}
|
||||
placeholder="System-Prompt für die KI"
|
||||
class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-800
|
||||
<!-- System Prompt -->
|
||||
<div>
|
||||
<label
|
||||
for="systemPrompt"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
System-Prompt *
|
||||
</label>
|
||||
<textarea
|
||||
id="systemPrompt"
|
||||
bind:value={systemPrompt}
|
||||
rows={5}
|
||||
placeholder="System-Prompt für die KI"
|
||||
class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-800
|
||||
text-gray-900 dark:text-white placeholder-gray-500
|
||||
{errors.systemPrompt ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||
></textarea>
|
||||
{#if errors.systemPrompt}
|
||||
<p class="mt-1 text-sm text-red-500">{errors.systemPrompt}</p>
|
||||
{:else}
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Der System-Prompt definiert die Rolle und das Verhalten der KI.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
></textarea>
|
||||
{#if errors.systemPrompt}
|
||||
<p class="mt-1 text-sm text-red-500">{errors.systemPrompt}</p>
|
||||
{:else}
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Der System-Prompt definiert die Rolle und das Verhalten der KI.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Initial Question -->
|
||||
<div>
|
||||
<label for="initialQuestion" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Beispielfrage (optional)
|
||||
</label>
|
||||
<textarea
|
||||
id="initialQuestion"
|
||||
bind:value={initialQuestion}
|
||||
rows={2}
|
||||
placeholder="Beispiel für eine passende Frage oder Anweisung"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
<!-- Initial Question -->
|
||||
<div>
|
||||
<label
|
||||
for="initialQuestion"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Beispielfrage (optional)
|
||||
</label>
|
||||
<textarea
|
||||
id="initialQuestion"
|
||||
bind:value={initialQuestion}
|
||||
rows={2}
|
||||
placeholder="Beispiel für eine passende Frage oder Anweisung"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Diese Frage wird als Vorschlag angezeigt, wenn die Vorlage ausgewählt wird.
|
||||
</p>
|
||||
</div>
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Diese Frage wird als Vorschlag angezeigt, wenn die Vorlage ausgewählt wird.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Color -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Farbe
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each TEMPLATE_COLORS as color}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (selectedColor = color)}
|
||||
class="w-8 h-8 rounded-full flex items-center justify-center transition-transform hover:scale-110
|
||||
{selectedColor === color ? 'ring-2 ring-offset-2 ring-gray-900 dark:ring-white' : ''}"
|
||||
style="background-color: {color}"
|
||||
aria-label="Farbe {color}"
|
||||
>
|
||||
{#if selectedColor === color}
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Color -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> Farbe </label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each TEMPLATE_COLORS as color}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (selectedColor = color)}
|
||||
class="w-8 h-8 rounded-full flex items-center justify-center transition-transform hover:scale-110
|
||||
{selectedColor === color
|
||||
? 'ring-2 ring-offset-2 ring-gray-900 dark:ring-white'
|
||||
: ''}"
|
||||
style="background-color: {color}"
|
||||
aria-label="Farbe {color}"
|
||||
>
|
||||
{#if selectedColor === color}
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="3"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model -->
|
||||
<div>
|
||||
<label for="model" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Bevorzugtes Modell (optional)
|
||||
</label>
|
||||
<select
|
||||
id="model"
|
||||
bind:value={selectedModelId}
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
<!-- Model -->
|
||||
<div>
|
||||
<label for="model" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Bevorzugtes Modell (optional)
|
||||
</label>
|
||||
<select
|
||||
id="model"
|
||||
bind:value={selectedModelId}
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">Kein Modell ausgewählt</option>
|
||||
{#each models as model}
|
||||
<option value={model.id}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Falls ausgewählt, wird dieses Modell automatisch mit der Vorlage verwendet.
|
||||
</p>
|
||||
</div>
|
||||
>
|
||||
<option value="">Kein Modell ausgewählt</option>
|
||||
{#each models as model}
|
||||
<option value={model.id}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Falls ausgewählt, wird dieses Modell automatisch mit der Vorlage verwendet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Document Mode -->
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (documentMode = !documentMode)}
|
||||
class="w-full flex items-center justify-between p-4 border rounded-lg transition-colors
|
||||
<!-- Document Mode -->
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (documentMode = !documentMode)}
|
||||
class="w-full flex items-center justify-between p-4 border rounded-lg transition-colors
|
||||
{documentMode
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800'}"
|
||||
>
|
||||
<div class="text-left">
|
||||
<p class="font-medium text-gray-900 dark:text-white">Dokumentmodus aktivieren</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">
|
||||
Ermöglicht die Bearbeitung eines Dokuments während der Konversation
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-6 h-6 rounded-full flex items-center justify-center
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800'}"
|
||||
>
|
||||
<div class="text-left">
|
||||
<p class="font-medium text-gray-900 dark:text-white">Dokumentmodus aktivieren</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">
|
||||
Ermöglicht die Bearbeitung eines Dokuments während der Konversation
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-6 h-6 rounded-full flex items-center justify-center
|
||||
{documentMode ? 'bg-blue-500' : 'bg-gray-400'}"
|
||||
>
|
||||
{#if documentMode}
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
>
|
||||
{#if documentMode}
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onclick={onCancel}
|
||||
class="flex-1 px-4 py-2.5 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300
|
||||
<!-- Buttons -->
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onclick={onCancel}
|
||||
class="flex-1 px-4 py-2.5 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300
|
||||
rounded-lg font-medium hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="flex-1 px-4 py-2.5 bg-blue-600 text-white rounded-lg font-medium
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="flex-1 px-4 py-2.5 bg-blue-600 text-white rounded-lg font-medium
|
||||
hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
{isEditMode ? 'Speichern' : 'Erstellen'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
>
|
||||
{isEditMode ? 'Speichern' : 'Erstellen'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,34 +2,40 @@
|
|||
* Chat Service - AI Completions via Backend API
|
||||
*/
|
||||
|
||||
import { chatApi, modelApi, type ChatMessage, type ChatCompletionResponse, type Model } from './api';
|
||||
import {
|
||||
chatApi,
|
||||
modelApi,
|
||||
type ChatMessage,
|
||||
type ChatCompletionResponse,
|
||||
type Model,
|
||||
} from './api';
|
||||
|
||||
export type { ChatMessage, ChatCompletionResponse };
|
||||
|
||||
export interface ChatCompletionRequest {
|
||||
messages: ChatMessage[];
|
||||
modelId: string;
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
messages: ChatMessage[];
|
||||
modelId: string;
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
}
|
||||
|
||||
export const chatService = {
|
||||
/**
|
||||
* Get available AI models
|
||||
*/
|
||||
async getModels(): Promise<Model[]> {
|
||||
return modelApi.getModels();
|
||||
},
|
||||
/**
|
||||
* Get available AI models
|
||||
*/
|
||||
async getModels(): Promise<Model[]> {
|
||||
return modelApi.getModels();
|
||||
},
|
||||
|
||||
/**
|
||||
* Send chat completion request
|
||||
*/
|
||||
async createCompletion(request: ChatCompletionRequest): Promise<ChatCompletionResponse | null> {
|
||||
return chatApi.createCompletion({
|
||||
messages: request.messages,
|
||||
modelId: request.modelId,
|
||||
temperature: request.temperature ?? 0.7,
|
||||
maxTokens: request.maxTokens ?? 1000,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Send chat completion request
|
||||
*/
|
||||
async createCompletion(request: ChatCompletionRequest): Promise<ChatCompletionResponse | null> {
|
||||
return chatApi.createCompletion({
|
||||
messages: request.messages,
|
||||
modelId: request.modelId,
|
||||
temperature: request.temperature ?? 0.7,
|
||||
maxTokens: request.maxTokens ?? 1000,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,197 +2,187 @@
|
|||
* Conversation Service - CRUD operations via Backend API
|
||||
*/
|
||||
|
||||
import {
|
||||
conversationApi,
|
||||
chatApi,
|
||||
type Conversation,
|
||||
type Message,
|
||||
type ChatMessage,
|
||||
} from './api';
|
||||
import { conversationApi, chatApi, type Conversation, type Message, type ChatMessage } from './api';
|
||||
|
||||
export type { Conversation, Message };
|
||||
|
||||
export const conversationService = {
|
||||
/**
|
||||
* Create a new conversation
|
||||
*/
|
||||
async createConversation(
|
||||
userId: string,
|
||||
modelId: string,
|
||||
mode: 'free' | 'guided' | 'template' = 'free',
|
||||
templateId?: string,
|
||||
documentMode: boolean = false,
|
||||
spaceId?: string,
|
||||
): Promise<string | null> {
|
||||
const conversation = await conversationApi.createConversation({
|
||||
modelId,
|
||||
conversationMode: mode,
|
||||
templateId,
|
||||
documentMode,
|
||||
spaceId,
|
||||
});
|
||||
/**
|
||||
* Create a new conversation
|
||||
*/
|
||||
async createConversation(
|
||||
userId: string,
|
||||
modelId: string,
|
||||
mode: 'free' | 'guided' | 'template' = 'free',
|
||||
templateId?: string,
|
||||
documentMode: boolean = false,
|
||||
spaceId?: string
|
||||
): Promise<string | null> {
|
||||
const conversation = await conversationApi.createConversation({
|
||||
modelId,
|
||||
conversationMode: mode,
|
||||
templateId,
|
||||
documentMode,
|
||||
spaceId,
|
||||
});
|
||||
|
||||
return conversation?.id || null;
|
||||
},
|
||||
return conversation?.id || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all active conversations for a user
|
||||
*/
|
||||
async getConversations(userId: string, spaceId?: string): Promise<Conversation[]> {
|
||||
return conversationApi.getConversations(spaceId);
|
||||
},
|
||||
/**
|
||||
* Get all active conversations for a user
|
||||
*/
|
||||
async getConversations(userId: string, spaceId?: string): Promise<Conversation[]> {
|
||||
return conversationApi.getConversations(spaceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get archived conversations
|
||||
*/
|
||||
async getArchivedConversations(userId: string): Promise<Conversation[]> {
|
||||
return conversationApi.getArchivedConversations();
|
||||
},
|
||||
/**
|
||||
* Get archived conversations
|
||||
*/
|
||||
async getArchivedConversations(userId: string): Promise<Conversation[]> {
|
||||
return conversationApi.getArchivedConversations();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single conversation
|
||||
*/
|
||||
async getConversation(conversationId: string): Promise<Conversation | null> {
|
||||
return conversationApi.getConversation(conversationId);
|
||||
},
|
||||
/**
|
||||
* Get a single conversation
|
||||
*/
|
||||
async getConversation(conversationId: string): Promise<Conversation | null> {
|
||||
return conversationApi.getConversation(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get messages for a conversation
|
||||
*/
|
||||
async getMessages(conversationId: string): Promise<Message[]> {
|
||||
return conversationApi.getMessages(conversationId);
|
||||
},
|
||||
/**
|
||||
* Get messages for a conversation
|
||||
*/
|
||||
async getMessages(conversationId: string): Promise<Message[]> {
|
||||
return conversationApi.getMessages(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a message to a conversation
|
||||
*/
|
||||
async addMessage(
|
||||
conversationId: string,
|
||||
sender: 'user' | 'assistant' | 'system',
|
||||
messageText: string,
|
||||
): Promise<string | null> {
|
||||
const message = await conversationApi.addMessage(conversationId, sender, messageText);
|
||||
return message?.id || null;
|
||||
},
|
||||
/**
|
||||
* Add a message to a conversation
|
||||
*/
|
||||
async addMessage(
|
||||
conversationId: string,
|
||||
sender: 'user' | 'assistant' | 'system',
|
||||
messageText: string
|
||||
): Promise<string | null> {
|
||||
const message = await conversationApi.addMessage(conversationId, sender, messageText);
|
||||
return message?.id || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update conversation title
|
||||
*/
|
||||
async updateTitle(conversationId: string, title: string): Promise<boolean> {
|
||||
return conversationApi.updateTitle(conversationId, title);
|
||||
},
|
||||
/**
|
||||
* Update conversation title
|
||||
*/
|
||||
async updateTitle(conversationId: string, title: string): Promise<boolean> {
|
||||
return conversationApi.updateTitle(conversationId, title);
|
||||
},
|
||||
|
||||
/**
|
||||
* Archive a conversation
|
||||
*/
|
||||
async archiveConversation(conversationId: string): Promise<boolean> {
|
||||
return conversationApi.archiveConversation(conversationId);
|
||||
},
|
||||
/**
|
||||
* Archive a conversation
|
||||
*/
|
||||
async archiveConversation(conversationId: string): Promise<boolean> {
|
||||
return conversationApi.archiveConversation(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Unarchive a conversation
|
||||
*/
|
||||
async unarchiveConversation(conversationId: string): Promise<boolean> {
|
||||
return conversationApi.unarchiveConversation(conversationId);
|
||||
},
|
||||
/**
|
||||
* Unarchive a conversation
|
||||
*/
|
||||
async unarchiveConversation(conversationId: string): Promise<boolean> {
|
||||
return conversationApi.unarchiveConversation(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a conversation permanently
|
||||
*/
|
||||
async deleteConversation(conversationId: string): Promise<boolean> {
|
||||
return conversationApi.deleteConversation(conversationId);
|
||||
},
|
||||
/**
|
||||
* Delete a conversation permanently
|
||||
*/
|
||||
async deleteConversation(conversationId: string): Promise<boolean> {
|
||||
return conversationApi.deleteConversation(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a message and get AI response
|
||||
*/
|
||||
async sendMessageAndGetResponse(
|
||||
conversationId: string,
|
||||
userMessage: string,
|
||||
modelId: string,
|
||||
): Promise<{
|
||||
userMessageId: string | null;
|
||||
assistantMessageId: string | null;
|
||||
assistantResponse: string;
|
||||
title?: string;
|
||||
}> {
|
||||
// Add user message
|
||||
const userMessageId = await this.addMessage(conversationId, 'user', userMessage);
|
||||
/**
|
||||
* Send a message and get AI response
|
||||
*/
|
||||
async sendMessageAndGetResponse(
|
||||
conversationId: string,
|
||||
userMessage: string,
|
||||
modelId: string
|
||||
): Promise<{
|
||||
userMessageId: string | null;
|
||||
assistantMessageId: string | null;
|
||||
assistantResponse: string;
|
||||
title?: string;
|
||||
}> {
|
||||
// Add user message
|
||||
const userMessageId = await this.addMessage(conversationId, 'user', userMessage);
|
||||
|
||||
// Load all messages for context
|
||||
const messages = await this.getMessages(conversationId);
|
||||
// Load all messages for context
|
||||
const messages = await this.getMessages(conversationId);
|
||||
|
||||
// Build chat messages for API
|
||||
const chatMessages: ChatMessage[] = messages.map((m) => ({
|
||||
role: m.sender === 'user' ? 'user' : m.sender === 'assistant' ? 'assistant' : 'system',
|
||||
content: m.messageText,
|
||||
}));
|
||||
// Build chat messages for API
|
||||
const chatMessages: ChatMessage[] = messages.map((m) => ({
|
||||
role: m.sender === 'user' ? 'user' : m.sender === 'assistant' ? 'assistant' : 'system',
|
||||
content: m.messageText,
|
||||
}));
|
||||
|
||||
// Get AI response
|
||||
const response = await chatApi.createCompletion({
|
||||
messages: chatMessages,
|
||||
modelId,
|
||||
});
|
||||
// Get AI response
|
||||
const response = await chatApi.createCompletion({
|
||||
messages: chatMessages,
|
||||
modelId,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
userMessageId,
|
||||
assistantMessageId: null,
|
||||
assistantResponse: 'Fehler beim Abrufen der Antwort.',
|
||||
};
|
||||
}
|
||||
if (!response) {
|
||||
return {
|
||||
userMessageId,
|
||||
assistantMessageId: null,
|
||||
assistantResponse: 'Fehler beim Abrufen der Antwort.',
|
||||
};
|
||||
}
|
||||
|
||||
// Save assistant message
|
||||
const assistantMessageId = await this.addMessage(
|
||||
conversationId,
|
||||
'assistant',
|
||||
response.content,
|
||||
);
|
||||
// Save assistant message
|
||||
const assistantMessageId = await this.addMessage(conversationId, 'assistant', response.content);
|
||||
|
||||
// Generate title if this is a new conversation (first or second message)
|
||||
let title: string | undefined;
|
||||
if (messages.length <= 2) {
|
||||
title = await this.generateTitle(userMessage);
|
||||
if (title) {
|
||||
await this.updateTitle(conversationId, title);
|
||||
}
|
||||
}
|
||||
// Generate title if this is a new conversation (first or second message)
|
||||
let title: string | undefined;
|
||||
if (messages.length <= 2) {
|
||||
title = await this.generateTitle(userMessage);
|
||||
if (title) {
|
||||
await this.updateTitle(conversationId, title);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userMessageId,
|
||||
assistantMessageId,
|
||||
assistantResponse: response.content,
|
||||
title,
|
||||
};
|
||||
},
|
||||
return {
|
||||
userMessageId,
|
||||
assistantMessageId,
|
||||
assistantResponse: response.content,
|
||||
title,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a conversation title based on user message
|
||||
*/
|
||||
async generateTitle(userMessage: string): Promise<string> {
|
||||
const titlePrompt = `Schreibe eine kurze, prägnante Überschrift (maximal 5 Wörter) für diesen Chat: "${userMessage}"`;
|
||||
/**
|
||||
* Generate a conversation title based on user message
|
||||
*/
|
||||
async generateTitle(userMessage: string): Promise<string> {
|
||||
const titlePrompt = `Schreibe eine kurze, prägnante Überschrift (maximal 5 Wörter) für diesen Chat: "${userMessage}"`;
|
||||
|
||||
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 title = response.content
|
||||
.trim()
|
||||
.replace(/^["']|["']$/g, '')
|
||||
.replace(/\.$/g, '');
|
||||
// Clean up title
|
||||
let title = response.content
|
||||
.trim()
|
||||
.replace(/^["']|["']$/g, '')
|
||||
.replace(/\.$/g, '');
|
||||
|
||||
if (title.length > 100) {
|
||||
title = title.substring(0, 97) + '...';
|
||||
}
|
||||
if (title.length > 100) {
|
||||
title = title.substring(0, 97) + '...';
|
||||
}
|
||||
|
||||
return title;
|
||||
},
|
||||
return title;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,84 +7,81 @@ import { documentApi, conversationApi, type Document } from './api';
|
|||
export type { Document };
|
||||
|
||||
export type DocumentWithConversation = Document & {
|
||||
conversation_title: string;
|
||||
conversation_title: string;
|
||||
};
|
||||
|
||||
export const documentService = {
|
||||
/**
|
||||
* Get all documents for a user (latest version of each)
|
||||
* This requires fetching conversations first, then documents
|
||||
*/
|
||||
async getUserDocuments(userId: string): Promise<DocumentWithConversation[]> {
|
||||
// Get all conversations (the API will filter by user)
|
||||
const conversations = await conversationApi.getConversations();
|
||||
/**
|
||||
* Get all documents for a user (latest version of each)
|
||||
* This requires fetching conversations first, then documents
|
||||
*/
|
||||
async getUserDocuments(userId: string): Promise<DocumentWithConversation[]> {
|
||||
// Get all conversations (the API will filter by user)
|
||||
const conversations = await conversationApi.getConversations();
|
||||
|
||||
// Filter to only document mode conversations
|
||||
const documentConversations = conversations.filter((c) => c.documentMode);
|
||||
// Filter to only document mode conversations
|
||||
const documentConversations = conversations.filter((c) => c.documentMode);
|
||||
|
||||
if (documentConversations.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (documentConversations.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// For each conversation, load the latest document version
|
||||
const documents: DocumentWithConversation[] = [];
|
||||
// For each conversation, load the latest document version
|
||||
const documents: DocumentWithConversation[] = [];
|
||||
|
||||
for (const conv of documentConversations) {
|
||||
const doc = await documentApi.getLatestDocument(conv.id);
|
||||
for (const conv of documentConversations) {
|
||||
const doc = await documentApi.getLatestDocument(conv.id);
|
||||
|
||||
if (doc) {
|
||||
documents.push({
|
||||
...doc,
|
||||
conversation_title: conv.title || 'Unbenannte Konversation',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (doc) {
|
||||
documents.push({
|
||||
...doc,
|
||||
conversation_title: conv.title || 'Unbenannte Konversation',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return documents;
|
||||
},
|
||||
return documents;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the latest document for a conversation
|
||||
*/
|
||||
async getLatestDocument(conversationId: string): Promise<Document | null> {
|
||||
return documentApi.getLatestDocument(conversationId);
|
||||
},
|
||||
/**
|
||||
* Get the latest document for a conversation
|
||||
*/
|
||||
async getLatestDocument(conversationId: string): Promise<Document | null> {
|
||||
return documentApi.getLatestDocument(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new document
|
||||
*/
|
||||
async createDocument(conversationId: string, content: string): Promise<Document | null> {
|
||||
return documentApi.createDocument(conversationId, content);
|
||||
},
|
||||
/**
|
||||
* Create a new document
|
||||
*/
|
||||
async createDocument(conversationId: string, content: string): Promise<Document | null> {
|
||||
return documentApi.createDocument(conversationId, content);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new version of a document
|
||||
*/
|
||||
async createDocumentVersion(
|
||||
conversationId: string,
|
||||
content: string,
|
||||
): Promise<Document | null> {
|
||||
return documentApi.createDocumentVersion(conversationId, content);
|
||||
},
|
||||
/**
|
||||
* Create a new version of a document
|
||||
*/
|
||||
async createDocumentVersion(conversationId: string, content: string): Promise<Document | null> {
|
||||
return documentApi.createDocumentVersion(conversationId, content);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all versions of a document
|
||||
*/
|
||||
async getAllDocumentVersions(conversationId: string): Promise<Document[]> {
|
||||
return documentApi.getAllDocumentVersions(conversationId);
|
||||
},
|
||||
/**
|
||||
* Get all versions of a document
|
||||
*/
|
||||
async getAllDocumentVersions(conversationId: string): Promise<Document[]> {
|
||||
return documentApi.getAllDocumentVersions(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a document exists for a conversation
|
||||
*/
|
||||
async hasDocument(conversationId: string): Promise<boolean> {
|
||||
return documentApi.hasDocument(conversationId);
|
||||
},
|
||||
/**
|
||||
* Check if a document exists for a conversation
|
||||
*/
|
||||
async hasDocument(conversationId: string): Promise<boolean> {
|
||||
return documentApi.hasDocument(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a specific document version
|
||||
*/
|
||||
async deleteDocumentVersion(documentId: string): Promise<boolean> {
|
||||
return documentApi.deleteDocumentVersion(documentId);
|
||||
},
|
||||
/**
|
||||
* Delete a specific document version
|
||||
*/
|
||||
async deleteDocumentVersion(documentId: string): Promise<boolean> {
|
||||
return documentApi.deleteDocumentVersion(documentId);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,118 +7,113 @@ import { spaceApi, type Space, type SpaceMember } from './api';
|
|||
export type { Space, SpaceMember };
|
||||
|
||||
export const spaceService = {
|
||||
/**
|
||||
* Get all spaces for the current user (both owned and member of)
|
||||
*/
|
||||
async getUserSpaces(userId: string): Promise<Space[]> {
|
||||
return spaceApi.getUserSpaces();
|
||||
},
|
||||
/**
|
||||
* Get all spaces for the current user (both owned and member of)
|
||||
*/
|
||||
async getUserSpaces(userId: string): Promise<Space[]> {
|
||||
return spaceApi.getUserSpaces();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single space by ID
|
||||
*/
|
||||
async getSpace(spaceId: string): Promise<Space | null> {
|
||||
return spaceApi.getSpace(spaceId);
|
||||
},
|
||||
/**
|
||||
* Get a single space by ID
|
||||
*/
|
||||
async getSpace(spaceId: string): Promise<Space | null> {
|
||||
return spaceApi.getSpace(spaceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new space
|
||||
*/
|
||||
async createSpace(space: {
|
||||
name: string;
|
||||
description?: string;
|
||||
owner_id: string;
|
||||
}): Promise<string | null> {
|
||||
const result = await spaceApi.createSpace(space.name, space.description);
|
||||
return result?.id || null;
|
||||
},
|
||||
/**
|
||||
* Create a new space
|
||||
*/
|
||||
async createSpace(space: {
|
||||
name: string;
|
||||
description?: string;
|
||||
owner_id: string;
|
||||
}): Promise<string | null> {
|
||||
const result = await spaceApi.createSpace(space.name, space.description);
|
||||
return result?.id || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a space
|
||||
*/
|
||||
async updateSpace(
|
||||
spaceId: string,
|
||||
updates: { name?: string; description?: string; isArchived?: boolean },
|
||||
): Promise<boolean> {
|
||||
return spaceApi.updateSpace(spaceId, updates);
|
||||
},
|
||||
/**
|
||||
* Update a space
|
||||
*/
|
||||
async updateSpace(
|
||||
spaceId: string,
|
||||
updates: { name?: string; description?: string; isArchived?: boolean }
|
||||
): Promise<boolean> {
|
||||
return spaceApi.updateSpace(spaceId, updates);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a space
|
||||
*/
|
||||
async deleteSpace(spaceId: string): Promise<boolean> {
|
||||
return spaceApi.deleteSpace(spaceId);
|
||||
},
|
||||
/**
|
||||
* Delete a space
|
||||
*/
|
||||
async deleteSpace(spaceId: string): Promise<boolean> {
|
||||
return spaceApi.deleteSpace(spaceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get members of a space
|
||||
*/
|
||||
async getSpaceMembers(spaceId: string): Promise<SpaceMember[]> {
|
||||
return spaceApi.getSpaceMembers(spaceId);
|
||||
},
|
||||
/**
|
||||
* Get members of a space
|
||||
*/
|
||||
async getSpaceMembers(spaceId: string): Promise<SpaceMember[]> {
|
||||
return spaceApi.getSpaceMembers(spaceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get user's role in a space
|
||||
*/
|
||||
async getUserRoleInSpace(
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
): Promise<'owner' | 'admin' | 'member' | 'viewer' | null> {
|
||||
return spaceApi.getUserRoleInSpace(spaceId);
|
||||
},
|
||||
/**
|
||||
* Get user's role in a space
|
||||
*/
|
||||
async getUserRoleInSpace(
|
||||
spaceId: string,
|
||||
userId: string
|
||||
): Promise<'owner' | 'admin' | 'member' | 'viewer' | null> {
|
||||
return spaceApi.getUserRoleInSpace(spaceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Leave a space (remove self from members)
|
||||
*/
|
||||
async leaveSpace(spaceId: string, userId: string): Promise<boolean> {
|
||||
return spaceApi.removeMember(spaceId, userId);
|
||||
},
|
||||
/**
|
||||
* Leave a space (remove self from members)
|
||||
*/
|
||||
async leaveSpace(spaceId: string, userId: string): Promise<boolean> {
|
||||
return spaceApi.removeMember(spaceId, userId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invite a user to a space
|
||||
*/
|
||||
async inviteUserToSpace(
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
role: 'admin' | 'member' | 'viewer' = 'member',
|
||||
): Promise<boolean> {
|
||||
return spaceApi.inviteUser(spaceId, userId, role);
|
||||
},
|
||||
/**
|
||||
* Invite a user to a space
|
||||
*/
|
||||
async inviteUserToSpace(
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
role: 'admin' | 'member' | 'viewer' = 'member'
|
||||
): Promise<boolean> {
|
||||
return spaceApi.inviteUser(spaceId, userId, role);
|
||||
},
|
||||
|
||||
/**
|
||||
* Respond to a space invitation
|
||||
*/
|
||||
async respondToInvitation(
|
||||
spaceId: string,
|
||||
status: 'accepted' | 'declined',
|
||||
): Promise<boolean> {
|
||||
return spaceApi.respondToInvitation(spaceId, status);
|
||||
},
|
||||
/**
|
||||
* Respond to a space invitation
|
||||
*/
|
||||
async respondToInvitation(spaceId: string, status: 'accepted' | 'declined'): Promise<boolean> {
|
||||
return spaceApi.respondToInvitation(spaceId, status);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get pending invitations for the current user
|
||||
*/
|
||||
async getPendingInvitations(): Promise<
|
||||
Array<{ invitation: SpaceMember; space: Space }>
|
||||
> {
|
||||
return spaceApi.getPendingInvitations();
|
||||
},
|
||||
/**
|
||||
* Get pending invitations for the current user
|
||||
*/
|
||||
async getPendingInvitations(): Promise<Array<{ invitation: SpaceMember; space: Space }>> {
|
||||
return spaceApi.getPendingInvitations();
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a member from a space
|
||||
*/
|
||||
async removeMember(spaceId: string, userId: string): Promise<boolean> {
|
||||
return spaceApi.removeMember(spaceId, userId);
|
||||
},
|
||||
/**
|
||||
* Remove a member from a space
|
||||
*/
|
||||
async removeMember(spaceId: string, userId: string): Promise<boolean> {
|
||||
return spaceApi.removeMember(spaceId, userId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Change a member's role
|
||||
*/
|
||||
async changeMemberRole(
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
newRole: 'admin' | 'member' | 'viewer',
|
||||
): Promise<boolean> {
|
||||
return spaceApi.changeMemberRole(spaceId, userId, newRole);
|
||||
},
|
||||
/**
|
||||
* Change a member's role
|
||||
*/
|
||||
async changeMemberRole(
|
||||
spaceId: string,
|
||||
userId: string,
|
||||
newRole: 'admin' | 'member' | 'viewer'
|
||||
): Promise<boolean> {
|
||||
return spaceApi.changeMemberRole(spaceId, userId, newRole);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,80 +7,80 @@ import { templateApi, type Template } from './api';
|
|||
export type { Template };
|
||||
|
||||
export const templateService = {
|
||||
/**
|
||||
* Get all templates for the current user
|
||||
*/
|
||||
async getTemplates(userId: string): Promise<Template[]> {
|
||||
return templateApi.getTemplates();
|
||||
},
|
||||
/**
|
||||
* Get all templates for the current user
|
||||
*/
|
||||
async getTemplates(userId: string): Promise<Template[]> {
|
||||
return templateApi.getTemplates();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single template by ID
|
||||
*/
|
||||
async getTemplate(templateId: string): Promise<Template | null> {
|
||||
return templateApi.getTemplate(templateId);
|
||||
},
|
||||
/**
|
||||
* Get a single template by ID
|
||||
*/
|
||||
async getTemplate(templateId: string): Promise<Template | null> {
|
||||
return templateApi.getTemplate(templateId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the default template for the current user
|
||||
*/
|
||||
async getDefaultTemplate(userId: string): Promise<Template | null> {
|
||||
return templateApi.getDefaultTemplate();
|
||||
},
|
||||
/**
|
||||
* Get the default template for the current user
|
||||
*/
|
||||
async getDefaultTemplate(userId: string): Promise<Template | null> {
|
||||
return templateApi.getDefaultTemplate();
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new template
|
||||
*/
|
||||
async createTemplate(template: {
|
||||
userId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
systemPrompt: string;
|
||||
initialQuestion?: string;
|
||||
modelId?: string;
|
||||
color?: string;
|
||||
documentMode?: boolean;
|
||||
}): Promise<Template | null> {
|
||||
return templateApi.createTemplate({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
systemPrompt: template.systemPrompt,
|
||||
initialQuestion: template.initialQuestion,
|
||||
modelId: template.modelId,
|
||||
color: template.color,
|
||||
documentMode: template.documentMode,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Create a new template
|
||||
*/
|
||||
async createTemplate(template: {
|
||||
userId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
systemPrompt: string;
|
||||
initialQuestion?: string;
|
||||
modelId?: string;
|
||||
color?: string;
|
||||
documentMode?: boolean;
|
||||
}): Promise<Template | null> {
|
||||
return templateApi.createTemplate({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
systemPrompt: template.systemPrompt,
|
||||
initialQuestion: template.initialQuestion,
|
||||
modelId: template.modelId,
|
||||
color: template.color,
|
||||
documentMode: template.documentMode,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing template
|
||||
*/
|
||||
async updateTemplate(
|
||||
templateId: string,
|
||||
updates: Partial<{
|
||||
name: string;
|
||||
description: string;
|
||||
systemPrompt: string;
|
||||
initialQuestion: string;
|
||||
modelId: string;
|
||||
color: string;
|
||||
documentMode: boolean;
|
||||
}>,
|
||||
): Promise<boolean> {
|
||||
return templateApi.updateTemplate(templateId, updates);
|
||||
},
|
||||
/**
|
||||
* Update an existing template
|
||||
*/
|
||||
async updateTemplate(
|
||||
templateId: string,
|
||||
updates: Partial<{
|
||||
name: string;
|
||||
description: string;
|
||||
systemPrompt: string;
|
||||
initialQuestion: string;
|
||||
modelId: string;
|
||||
color: string;
|
||||
documentMode: boolean;
|
||||
}>
|
||||
): Promise<boolean> {
|
||||
return templateApi.updateTemplate(templateId, updates);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a template
|
||||
*/
|
||||
async deleteTemplate(templateId: string): Promise<boolean> {
|
||||
return templateApi.deleteTemplate(templateId);
|
||||
},
|
||||
/**
|
||||
* Delete a template
|
||||
*/
|
||||
async deleteTemplate(templateId: string): Promise<boolean> {
|
||||
return templateApi.deleteTemplate(templateId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a template as default
|
||||
*/
|
||||
async setDefaultTemplate(templateId: string, userId: string): Promise<boolean> {
|
||||
return templateApi.setDefaultTemplate(templateId);
|
||||
},
|
||||
/**
|
||||
* Set a template as default
|
||||
*/
|
||||
async setDefaultTemplate(templateId: string, userId: string): Promise<boolean> {
|
||||
return templateApi.setDefaultTemplate(templateId);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = n
|
|||
let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null = null;
|
||||
|
||||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
return _authService;
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
return _authService;
|
||||
}
|
||||
|
||||
// State
|
||||
|
|
@ -30,175 +30,175 @@ let loading = $state(true);
|
|||
let initialized = $state(false);
|
||||
|
||||
export const authStore = {
|
||||
// Getters
|
||||
get user() {
|
||||
return user;
|
||||
},
|
||||
get loading() {
|
||||
return loading;
|
||||
},
|
||||
get isAuthenticated() {
|
||||
return !!user;
|
||||
},
|
||||
get initialized() {
|
||||
return initialized;
|
||||
},
|
||||
// Getters
|
||||
get user() {
|
||||
return user;
|
||||
},
|
||||
get loading() {
|
||||
return loading;
|
||||
},
|
||||
get isAuthenticated() {
|
||||
return !!user;
|
||||
},
|
||||
get initialized() {
|
||||
return initialized;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize auth state from stored tokens
|
||||
*/
|
||||
async initialize() {
|
||||
if (initialized) return;
|
||||
/**
|
||||
* Initialize auth state from stored tokens
|
||||
*/
|
||||
async initialize() {
|
||||
if (initialized) return;
|
||||
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
initialized = true;
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
initialized = true;
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
const authenticated = await authService.isAuthenticated();
|
||||
if (authenticated) {
|
||||
const userData = await authService.getUserFromToken();
|
||||
user = userData;
|
||||
}
|
||||
initialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize auth:', error);
|
||||
user = null;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
loading = true;
|
||||
try {
|
||||
const authenticated = await authService.isAuthenticated();
|
||||
if (authenticated) {
|
||||
const userData = await authService.getUserFromToken();
|
||||
user = userData;
|
||||
}
|
||||
initialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize auth:', error);
|
||||
user = null;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sign in with email and password
|
||||
*/
|
||||
async signIn(email: string, password: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return { success: false, error: 'Auth not available on server' };
|
||||
}
|
||||
/**
|
||||
* Sign in with email and password
|
||||
*/
|
||||
async signIn(email: string, password: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return { success: false, error: 'Auth not available on server' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await authService.signIn(email, password);
|
||||
try {
|
||||
const result = await authService.signIn(email, password);
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Login failed' };
|
||||
}
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Login failed' };
|
||||
}
|
||||
|
||||
// Get user data from token
|
||||
const userData = await authService.getUserFromToken();
|
||||
user = userData;
|
||||
// Get user data from token
|
||||
const userData = await authService.getUserFromToken();
|
||||
user = userData;
|
||||
|
||||
return { success: true, error: null };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
},
|
||||
return { success: true, error: null };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sign up with email and password
|
||||
*/
|
||||
async signUp(email: string, password: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return { success: false, error: 'Auth not available on server', needsVerification: false };
|
||||
}
|
||||
/**
|
||||
* Sign up with email and password
|
||||
*/
|
||||
async signUp(email: string, password: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return { success: false, error: 'Auth not available on server', needsVerification: false };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await authService.signUp(email, password);
|
||||
try {
|
||||
const result = await authService.signUp(email, password);
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Signup failed', needsVerification: false };
|
||||
}
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Signup failed', needsVerification: false };
|
||||
}
|
||||
|
||||
// Mana Core Auth requires separate login after signup
|
||||
if (result.needsVerification) {
|
||||
return { success: true, error: null, needsVerification: true };
|
||||
}
|
||||
// Mana Core Auth requires separate login after signup
|
||||
if (result.needsVerification) {
|
||||
return { success: true, error: null, needsVerification: true };
|
||||
}
|
||||
|
||||
// Auto sign in after successful signup
|
||||
const signInResult = await this.signIn(email, password);
|
||||
return { ...signInResult, needsVerification: false };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: errorMessage, needsVerification: false };
|
||||
}
|
||||
},
|
||||
// Auto sign in after successful signup
|
||||
const signInResult = await this.signIn(email, password);
|
||||
return { ...signInResult, needsVerification: false };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: errorMessage, needsVerification: false };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sign out
|
||||
*/
|
||||
async signOut() {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
user = null;
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Sign out
|
||||
*/
|
||||
async signOut() {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
user = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await authService.signOut();
|
||||
user = null;
|
||||
} catch (error) {
|
||||
console.error('Sign out error:', error);
|
||||
// Clear user even if sign out fails
|
||||
user = null;
|
||||
}
|
||||
},
|
||||
try {
|
||||
await authService.signOut();
|
||||
user = null;
|
||||
} catch (error) {
|
||||
console.error('Sign out error:', error);
|
||||
// Clear user even if sign out fails
|
||||
user = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send password reset email
|
||||
*/
|
||||
async resetPassword(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return { success: false, error: 'Auth not available on server' };
|
||||
}
|
||||
/**
|
||||
* Send password reset email
|
||||
*/
|
||||
async resetPassword(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return { success: false, error: 'Auth not available on server' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await authService.forgotPassword(email);
|
||||
try {
|
||||
const result = await authService.forgotPassword(email);
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Password reset failed' };
|
||||
}
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Password reset failed' };
|
||||
}
|
||||
|
||||
return { success: true, error: null };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
},
|
||||
return { success: true, error: null };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get user credit balance
|
||||
*/
|
||||
async getCredits() {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Get user credit balance
|
||||
*/
|
||||
async getCredits() {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const credits = await authService.getUserCredits();
|
||||
return credits;
|
||||
} catch (error) {
|
||||
console.error('Failed to get credits:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
try {
|
||||
const credits = await authService.getUserCredits();
|
||||
return credits;
|
||||
} catch (error) {
|
||||
console.error('Failed to get credits:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get access token for API calls
|
||||
*/
|
||||
async getAccessToken() {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return null;
|
||||
}
|
||||
return await authService.getAppToken();
|
||||
},
|
||||
/**
|
||||
* Get access token for API calls
|
||||
*/
|
||||
async getAccessToken() {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return null;
|
||||
}
|
||||
return await authService.getAppToken();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,109 +17,109 @@ let error = $state<string | null>(null);
|
|||
let messageCounter = 0;
|
||||
|
||||
export const chatStore = {
|
||||
// Getters
|
||||
get messages() {
|
||||
return messages;
|
||||
},
|
||||
get models() {
|
||||
return models;
|
||||
},
|
||||
get selectedModelId() {
|
||||
return selectedModelId;
|
||||
},
|
||||
get selectedModel() {
|
||||
return models.find((m) => m.id === selectedModelId) || null;
|
||||
},
|
||||
get isLoading() {
|
||||
return isLoading;
|
||||
},
|
||||
get isSending() {
|
||||
return isSending;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
// Getters
|
||||
get messages() {
|
||||
return messages;
|
||||
},
|
||||
get models() {
|
||||
return models;
|
||||
},
|
||||
get selectedModelId() {
|
||||
return selectedModelId;
|
||||
},
|
||||
get selectedModel() {
|
||||
return models.find((m) => m.id === selectedModelId) || null;
|
||||
},
|
||||
get isLoading() {
|
||||
return isLoading;
|
||||
},
|
||||
get isSending() {
|
||||
return isSending;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
|
||||
// Actions
|
||||
async loadModels() {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
try {
|
||||
models = await chatService.getModels();
|
||||
if (models.length > 0 && !selectedModelId) {
|
||||
selectedModelId = models[0].id;
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load models';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
},
|
||||
// Actions
|
||||
async loadModels() {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
try {
|
||||
models = await chatService.getModels();
|
||||
if (models.length > 0 && !selectedModelId) {
|
||||
selectedModelId = models[0].id;
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load models';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
setSelectedModel(modelId: string) {
|
||||
selectedModelId = modelId;
|
||||
},
|
||||
setSelectedModel(modelId: string) {
|
||||
selectedModelId = modelId;
|
||||
},
|
||||
|
||||
async sendMessage(text: string) {
|
||||
if (!text.trim() || !selectedModelId) return;
|
||||
async sendMessage(text: string) {
|
||||
if (!text.trim() || !selectedModelId) return;
|
||||
|
||||
isSending = true;
|
||||
error = null;
|
||||
isSending = true;
|
||||
error = null;
|
||||
|
||||
// Add user message
|
||||
const userMessage: Message = {
|
||||
id: `temp-${++messageCounter}`,
|
||||
conversation_id: '',
|
||||
sender: 'user',
|
||||
message_text: text,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
messages = [...messages, userMessage];
|
||||
// Add user message
|
||||
const userMessage: Message = {
|
||||
id: `temp-${++messageCounter}`,
|
||||
conversation_id: '',
|
||||
sender: 'user',
|
||||
message_text: text,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
messages = [...messages, userMessage];
|
||||
|
||||
try {
|
||||
// Build chat messages for API
|
||||
const chatMessages: ChatMessage[] = messages.map((m) => ({
|
||||
role: m.sender === 'user' ? 'user' : 'assistant',
|
||||
content: m.message_text,
|
||||
}));
|
||||
try {
|
||||
// Build chat messages for API
|
||||
const chatMessages: ChatMessage[] = messages.map((m) => ({
|
||||
role: m.sender === 'user' ? 'user' : 'assistant',
|
||||
content: m.message_text,
|
||||
}));
|
||||
|
||||
const request: ChatCompletionRequest = {
|
||||
messages: chatMessages,
|
||||
modelId: selectedModelId,
|
||||
};
|
||||
const request: ChatCompletionRequest = {
|
||||
messages: chatMessages,
|
||||
modelId: selectedModelId,
|
||||
};
|
||||
|
||||
const response = await chatService.createCompletion(request);
|
||||
const response = await chatService.createCompletion(request);
|
||||
|
||||
if (response) {
|
||||
// Add assistant message
|
||||
const assistantMessage: Message = {
|
||||
id: `temp-${++messageCounter}`,
|
||||
conversation_id: '',
|
||||
sender: 'assistant',
|
||||
message_text: response.content,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
messages = [...messages, assistantMessage];
|
||||
} else {
|
||||
error = 'Failed to get response';
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to send message';
|
||||
} finally {
|
||||
isSending = false;
|
||||
}
|
||||
},
|
||||
if (response) {
|
||||
// Add assistant message
|
||||
const assistantMessage: Message = {
|
||||
id: `temp-${++messageCounter}`,
|
||||
conversation_id: '',
|
||||
sender: 'assistant',
|
||||
message_text: response.content,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
messages = [...messages, assistantMessage];
|
||||
} else {
|
||||
error = 'Failed to get response';
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to send message';
|
||||
} finally {
|
||||
isSending = false;
|
||||
}
|
||||
},
|
||||
|
||||
clearMessages() {
|
||||
messages = [];
|
||||
messageCounter = 0;
|
||||
error = null;
|
||||
},
|
||||
clearMessages() {
|
||||
messages = [];
|
||||
messageCounter = 0;
|
||||
error = null;
|
||||
},
|
||||
|
||||
reset() {
|
||||
messages = [];
|
||||
messageCounter = 0;
|
||||
error = null;
|
||||
isSending = false;
|
||||
},
|
||||
reset() {
|
||||
messages = [];
|
||||
messageCounter = 0;
|
||||
error = null;
|
||||
isSending = false;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,124 +12,122 @@ let isLoading = $state(false);
|
|||
let error = $state<string | null>(null);
|
||||
|
||||
export const conversationsStore = {
|
||||
// Getters
|
||||
get conversations() {
|
||||
return conversations;
|
||||
},
|
||||
get archivedConversations() {
|
||||
return archivedConversations;
|
||||
},
|
||||
get isLoading() {
|
||||
return isLoading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
// Getters
|
||||
get conversations() {
|
||||
return conversations;
|
||||
},
|
||||
get archivedConversations() {
|
||||
return archivedConversations;
|
||||
},
|
||||
get isLoading() {
|
||||
return isLoading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load conversations for a user
|
||||
*/
|
||||
async loadConversations(userId: string, spaceId?: string) {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
/**
|
||||
* Load conversations for a user
|
||||
*/
|
||||
async loadConversations(userId: string, spaceId?: string) {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
conversations = await conversationService.getConversations(userId, spaceId);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load conversations';
|
||||
conversations = [];
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
},
|
||||
try {
|
||||
conversations = await conversationService.getConversations(userId, spaceId);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load conversations';
|
||||
conversations = [];
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load archived conversations
|
||||
*/
|
||||
async loadArchivedConversations(userId: string) {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
/**
|
||||
* Load archived conversations
|
||||
*/
|
||||
async loadArchivedConversations(userId: string) {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
archivedConversations = await conversationService.getArchivedConversations(userId);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load archived conversations';
|
||||
archivedConversations = [];
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
},
|
||||
try {
|
||||
archivedConversations = await conversationService.getArchivedConversations(userId);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load archived conversations';
|
||||
archivedConversations = [];
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new conversation to the list
|
||||
*/
|
||||
addConversation(conversation: Conversation) {
|
||||
conversations = [conversation, ...conversations];
|
||||
},
|
||||
/**
|
||||
* Add a new conversation to the list
|
||||
*/
|
||||
addConversation(conversation: Conversation) {
|
||||
conversations = [conversation, ...conversations];
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a conversation in the list
|
||||
*/
|
||||
updateConversation(conversationId: string, updates: Partial<Conversation>) {
|
||||
conversations = conversations.map((c) =>
|
||||
c.id === conversationId ? { ...c, ...updates } : c
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Update a conversation in the list
|
||||
*/
|
||||
updateConversation(conversationId: string, updates: Partial<Conversation>) {
|
||||
conversations = conversations.map((c) => (c.id === conversationId ? { ...c, ...updates } : c));
|
||||
},
|
||||
|
||||
/**
|
||||
* Archive a conversation
|
||||
*/
|
||||
async archiveConversation(conversationId: string) {
|
||||
const success = await conversationService.archiveConversation(conversationId);
|
||||
/**
|
||||
* Archive a conversation
|
||||
*/
|
||||
async archiveConversation(conversationId: string) {
|
||||
const success = await conversationService.archiveConversation(conversationId);
|
||||
|
||||
if (success) {
|
||||
const conversation = conversations.find((c) => c.id === conversationId);
|
||||
if (conversation) {
|
||||
conversations = conversations.filter((c) => c.id !== conversationId);
|
||||
archivedConversations = [{ ...conversation, is_archived: true }, ...archivedConversations];
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
const conversation = conversations.find((c) => c.id === conversationId);
|
||||
if (conversation) {
|
||||
conversations = conversations.filter((c) => c.id !== conversationId);
|
||||
archivedConversations = [{ ...conversation, is_archived: true }, ...archivedConversations];
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
},
|
||||
return success;
|
||||
},
|
||||
|
||||
/**
|
||||
* Unarchive a conversation
|
||||
*/
|
||||
async unarchiveConversation(conversationId: string) {
|
||||
const success = await conversationService.unarchiveConversation(conversationId);
|
||||
/**
|
||||
* Unarchive a conversation
|
||||
*/
|
||||
async unarchiveConversation(conversationId: string) {
|
||||
const success = await conversationService.unarchiveConversation(conversationId);
|
||||
|
||||
if (success) {
|
||||
const conversation = archivedConversations.find((c) => c.id === conversationId);
|
||||
if (conversation) {
|
||||
archivedConversations = archivedConversations.filter((c) => c.id !== conversationId);
|
||||
conversations = [{ ...conversation, is_archived: false }, ...conversations];
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
const conversation = archivedConversations.find((c) => c.id === conversationId);
|
||||
if (conversation) {
|
||||
archivedConversations = archivedConversations.filter((c) => c.id !== conversationId);
|
||||
conversations = [{ ...conversation, is_archived: false }, ...conversations];
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
},
|
||||
return success;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a conversation
|
||||
*/
|
||||
async deleteConversation(conversationId: string) {
|
||||
const success = await conversationService.deleteConversation(conversationId);
|
||||
/**
|
||||
* Delete a conversation
|
||||
*/
|
||||
async deleteConversation(conversationId: string) {
|
||||
const success = await conversationService.deleteConversation(conversationId);
|
||||
|
||||
if (success) {
|
||||
conversations = conversations.filter((c) => c.id !== conversationId);
|
||||
archivedConversations = archivedConversations.filter((c) => c.id !== conversationId);
|
||||
}
|
||||
if (success) {
|
||||
conversations = conversations.filter((c) => c.id !== conversationId);
|
||||
archivedConversations = archivedConversations.filter((c) => c.id !== conversationId);
|
||||
}
|
||||
|
||||
return success;
|
||||
},
|
||||
return success;
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all data
|
||||
*/
|
||||
reset() {
|
||||
conversations = [];
|
||||
archivedConversations = [];
|
||||
error = null;
|
||||
},
|
||||
/**
|
||||
* Clear all data
|
||||
*/
|
||||
reset() {
|
||||
conversations = [];
|
||||
archivedConversations = [];
|
||||
error = null;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,112 +11,112 @@ let isLoading = $state(false);
|
|||
let error = $state<string | null>(null);
|
||||
|
||||
export const spacesStore = {
|
||||
// Getters
|
||||
get spaces() {
|
||||
return spaces;
|
||||
},
|
||||
get isLoading() {
|
||||
return isLoading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
// Getters
|
||||
get spaces() {
|
||||
return spaces;
|
||||
},
|
||||
get isLoading() {
|
||||
return isLoading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load spaces for a user
|
||||
*/
|
||||
async loadSpaces(userId: string) {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
/**
|
||||
* Load spaces for a user
|
||||
*/
|
||||
async loadSpaces(userId: string) {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
spaces = await spaceService.getUserSpaces(userId);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load spaces';
|
||||
spaces = [];
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
},
|
||||
try {
|
||||
spaces = await spaceService.getUserSpaces(userId);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load spaces';
|
||||
spaces = [];
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new space
|
||||
*/
|
||||
async createSpace(space: SpaceCreate): Promise<string | null> {
|
||||
error = null;
|
||||
/**
|
||||
* Create a new space
|
||||
*/
|
||||
async createSpace(space: SpaceCreate): Promise<string | null> {
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const spaceId = await spaceService.createSpace(space);
|
||||
if (spaceId) {
|
||||
// Reload spaces to get the new one with full data
|
||||
await this.loadSpaces(space.owner_id);
|
||||
}
|
||||
return spaceId;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create space';
|
||||
return null;
|
||||
}
|
||||
},
|
||||
try {
|
||||
const spaceId = await spaceService.createSpace(space);
|
||||
if (spaceId) {
|
||||
// Reload spaces to get the new one with full data
|
||||
await this.loadSpaces(space.owner_id);
|
||||
}
|
||||
return spaceId;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create space';
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a space
|
||||
*/
|
||||
async updateSpace(spaceId: string, updates: SpaceUpdate): Promise<boolean> {
|
||||
error = null;
|
||||
/**
|
||||
* Update a space
|
||||
*/
|
||||
async updateSpace(spaceId: string, updates: SpaceUpdate): Promise<boolean> {
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const success = await spaceService.updateSpace(spaceId, updates);
|
||||
if (success) {
|
||||
spaces = spaces.map((s) => (s.id === spaceId ? { ...s, ...updates } : s));
|
||||
}
|
||||
return success;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update space';
|
||||
return false;
|
||||
}
|
||||
},
|
||||
try {
|
||||
const success = await spaceService.updateSpace(spaceId, updates);
|
||||
if (success) {
|
||||
spaces = spaces.map((s) => (s.id === spaceId ? { ...s, ...updates } : s));
|
||||
}
|
||||
return success;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update space';
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a space
|
||||
*/
|
||||
async deleteSpace(spaceId: string): Promise<boolean> {
|
||||
error = null;
|
||||
/**
|
||||
* Delete a space
|
||||
*/
|
||||
async deleteSpace(spaceId: string): Promise<boolean> {
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const success = await spaceService.deleteSpace(spaceId);
|
||||
if (success) {
|
||||
spaces = spaces.filter((s) => s.id !== spaceId);
|
||||
}
|
||||
return success;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to delete space';
|
||||
return false;
|
||||
}
|
||||
},
|
||||
try {
|
||||
const success = await spaceService.deleteSpace(spaceId);
|
||||
if (success) {
|
||||
spaces = spaces.filter((s) => s.id !== spaceId);
|
||||
}
|
||||
return success;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to delete space';
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Leave a space
|
||||
*/
|
||||
async leaveSpace(spaceId: string, userId: string): Promise<boolean> {
|
||||
error = null;
|
||||
/**
|
||||
* Leave a space
|
||||
*/
|
||||
async leaveSpace(spaceId: string, userId: string): Promise<boolean> {
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const success = await spaceService.leaveSpace(spaceId, userId);
|
||||
if (success) {
|
||||
spaces = spaces.filter((s) => s.id !== spaceId);
|
||||
}
|
||||
return success;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to leave space';
|
||||
return false;
|
||||
}
|
||||
},
|
||||
try {
|
||||
const success = await spaceService.leaveSpace(spaceId, userId);
|
||||
if (success) {
|
||||
spaces = spaces.filter((s) => s.id !== spaceId);
|
||||
}
|
||||
return success;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to leave space';
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset store
|
||||
*/
|
||||
reset() {
|
||||
spaces = [];
|
||||
error = null;
|
||||
},
|
||||
/**
|
||||
* Reset store
|
||||
*/
|
||||
reset() {
|
||||
spaces = [];
|
||||
error = null;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,116 +11,116 @@ let isLoading = $state(false);
|
|||
let error = $state<string | null>(null);
|
||||
|
||||
export const templatesStore = {
|
||||
// Getters
|
||||
get templates() {
|
||||
return templates;
|
||||
},
|
||||
get isLoading() {
|
||||
return isLoading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
// Getters
|
||||
get templates() {
|
||||
return templates;
|
||||
},
|
||||
get isLoading() {
|
||||
return isLoading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load templates for a user
|
||||
*/
|
||||
async loadTemplates(userId: string) {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
/**
|
||||
* Load templates for a user
|
||||
*/
|
||||
async loadTemplates(userId: string) {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
templates = await templateService.getTemplates(userId);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load templates';
|
||||
templates = [];
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
},
|
||||
try {
|
||||
templates = await templateService.getTemplates(userId);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load templates';
|
||||
templates = [];
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new template
|
||||
*/
|
||||
async createTemplate(template: TemplateCreate): Promise<Template | null> {
|
||||
error = null;
|
||||
/**
|
||||
* Create a new template
|
||||
*/
|
||||
async createTemplate(template: TemplateCreate): Promise<Template | null> {
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const newTemplate = await templateService.createTemplate(template);
|
||||
if (newTemplate) {
|
||||
templates = [...templates, newTemplate].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
return newTemplate;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create template';
|
||||
return null;
|
||||
}
|
||||
},
|
||||
try {
|
||||
const newTemplate = await templateService.createTemplate(template);
|
||||
if (newTemplate) {
|
||||
templates = [...templates, newTemplate].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
return newTemplate;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create template';
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a template
|
||||
*/
|
||||
async updateTemplate(templateId: string, updates: TemplateUpdate): Promise<boolean> {
|
||||
error = null;
|
||||
/**
|
||||
* Update a template
|
||||
*/
|
||||
async updateTemplate(templateId: string, updates: TemplateUpdate): Promise<boolean> {
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const success = await templateService.updateTemplate(templateId, updates);
|
||||
if (success) {
|
||||
templates = templates
|
||||
.map((t) => (t.id === templateId ? { ...t, ...updates } : t))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
return success;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update template';
|
||||
return false;
|
||||
}
|
||||
},
|
||||
try {
|
||||
const success = await templateService.updateTemplate(templateId, updates);
|
||||
if (success) {
|
||||
templates = templates
|
||||
.map((t) => (t.id === templateId ? { ...t, ...updates } : t))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
return success;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update template';
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a template
|
||||
*/
|
||||
async deleteTemplate(templateId: string): Promise<boolean> {
|
||||
error = null;
|
||||
/**
|
||||
* Delete a template
|
||||
*/
|
||||
async deleteTemplate(templateId: string): Promise<boolean> {
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const success = await templateService.deleteTemplate(templateId);
|
||||
if (success) {
|
||||
templates = templates.filter((t) => t.id !== templateId);
|
||||
}
|
||||
return success;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to delete template';
|
||||
return false;
|
||||
}
|
||||
},
|
||||
try {
|
||||
const success = await templateService.deleteTemplate(templateId);
|
||||
if (success) {
|
||||
templates = templates.filter((t) => t.id !== templateId);
|
||||
}
|
||||
return success;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to delete template';
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a template as default
|
||||
*/
|
||||
async setDefaultTemplate(templateId: string, userId: string): Promise<boolean> {
|
||||
error = null;
|
||||
/**
|
||||
* Set a template as default
|
||||
*/
|
||||
async setDefaultTemplate(templateId: string, userId: string): Promise<boolean> {
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const success = await templateService.setDefaultTemplate(templateId, userId);
|
||||
if (success) {
|
||||
templates = templates.map((t) => ({
|
||||
...t,
|
||||
is_default: t.id === templateId,
|
||||
}));
|
||||
}
|
||||
return success;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to set default template';
|
||||
return false;
|
||||
}
|
||||
},
|
||||
try {
|
||||
const success = await templateService.setDefaultTemplate(templateId, userId);
|
||||
if (success) {
|
||||
templates = templates.map((t) => ({
|
||||
...t,
|
||||
is_default: t.id === templateId,
|
||||
}));
|
||||
}
|
||||
return success;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to set default template';
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset store
|
||||
*/
|
||||
reset() {
|
||||
templates = [];
|
||||
error = null;
|
||||
},
|
||||
/**
|
||||
* Reset store
|
||||
*/
|
||||
reset() {
|
||||
templates = [];
|
||||
error = null;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { createThemeStore } from '@manacore/shared-theme';
|
||||
|
||||
export const theme = createThemeStore({
|
||||
appId: 'chat',
|
||||
defaultVariant: 'ocean',
|
||||
primaryColor: {
|
||||
light: '217 91% 60%', // Blue
|
||||
dark: '217 91% 60%',
|
||||
},
|
||||
appId: 'chat',
|
||||
defaultVariant: 'ocean',
|
||||
primaryColor: {
|
||||
light: '217 91% 60%', // Blue
|
||||
dark: '217 91% 60%',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<script lang="ts">
|
||||
let { children } = $props();
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4 py-12">
|
||||
<div class="w-full max-w-md">
|
||||
{@render children()}
|
||||
</div>
|
||||
<div class="w-full max-w-md">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,101 +1,116 @@
|
|||
<script lang="ts">
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
let email = $state('');
|
||||
let error = $state<string | null>(null);
|
||||
let loading = $state(false);
|
||||
let success = $state(false);
|
||||
let email = $state('');
|
||||
let error = $state<string | null>(null);
|
||||
let loading = $state(false);
|
||||
let success = $state(false);
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = null;
|
||||
loading = true;
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = null;
|
||||
loading = true;
|
||||
|
||||
const result = await authStore.resetPassword(email);
|
||||
const result = await authStore.resetPassword(email);
|
||||
|
||||
if (result.success) {
|
||||
success = true;
|
||||
} else {
|
||||
error = result.error || 'Fehler beim Zurücksetzen des Passworts';
|
||||
}
|
||||
if (result.success) {
|
||||
success = true;
|
||||
} else {
|
||||
error = result.error || 'Fehler beim Zurücksetzen des Passworts';
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Passwort zurücksetzen | ManaChat</title>
|
||||
<title>Passwort zurücksetzen | ManaChat</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Passwort zurücksetzen</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-2">
|
||||
Gib deine E-Mail-Adresse ein, um dein Passwort zurückzusetzen
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Passwort zurücksetzen</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-2">
|
||||
Gib deine E-Mail-Adresse ein, um dein Passwort zurückzusetzen
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if success}
|
||||
<div class="p-4 bg-green-100 dark:bg-green-900/30 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<p class="text-green-700 dark:text-green-400 text-center">
|
||||
Wir haben dir eine E-Mail mit einem Link zum Zurücksetzen deines Passworts gesendet.
|
||||
</p>
|
||||
<div class="mt-4 text-center">
|
||||
<a href="/login" class="text-blue-600 dark:text-blue-400 hover:underline font-medium">
|
||||
Zurück zur Anmeldung
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if error}
|
||||
<div class="mb-4 p-3 bg-red-100 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
{#if success}
|
||||
<div
|
||||
class="p-4 bg-green-100 dark:bg-green-900/30 border border-green-200 dark:border-green-800 rounded-lg"
|
||||
>
|
||||
<p class="text-green-700 dark:text-green-400 text-center">
|
||||
Wir haben dir eine E-Mail mit einem Link zum Zurücksetzen deines Passworts gesendet.
|
||||
</p>
|
||||
<div class="mt-4 text-center">
|
||||
<a href="/login" class="text-blue-600 dark:text-blue-400 hover:underline font-medium">
|
||||
Zurück zur Anmeldung
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if error}
|
||||
<div
|
||||
class="mb-4 p-3 bg-red-100 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400 text-sm"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
E-Mail
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
bind:value={email}
|
||||
required
|
||||
disabled={loading}
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
E-Mail
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
bind:value={email}
|
||||
required
|
||||
disabled={loading}
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
placeholder="deine@email.de"
|
||||
/>
|
||||
</div>
|
||||
placeholder="deine@email.de"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg
|
||||
transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{#if loading}
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Wird gesendet...
|
||||
</span>
|
||||
{:else}
|
||||
Link senden
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
>
|
||||
{#if loading}
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
Wird gesendet...
|
||||
</span>
|
||||
{:else}
|
||||
Link senden
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<a href="/login" class="text-gray-600 dark:text-gray-400 hover:underline">
|
||||
Zurück zur Anmeldung
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-6 text-center">
|
||||
<a href="/login" class="text-gray-600 dark:text-gray-400 hover:underline">
|
||||
Zurück zur Anmeldung
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
signInFailed: 'Anmeldung fehlgeschlagen',
|
||||
googleSignInFailed: 'Google-Anmeldung fehlgeschlagen',
|
||||
signInSuccess: 'Erfolgreich angemeldet. Weiterleitung...',
|
||||
googleSignInSuccess: 'Erfolgreich mit Google angemeldet. Weiterleitung...'
|
||||
googleSignInSuccess: 'Erfolgreich mit Google angemeldet. Weiterleitung...',
|
||||
};
|
||||
|
||||
async function handleSignIn(email: string, password: string) {
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
logo={ChatLogo}
|
||||
primaryColor="#0ea5e9"
|
||||
onSignIn={handleSignIn}
|
||||
goto={goto}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
|
|
|
|||
|
|
@ -1,156 +1,177 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
let confirmPassword = $state('');
|
||||
let error = $state<string | null>(null);
|
||||
let loading = $state(false);
|
||||
let success = $state(false);
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
let confirmPassword = $state('');
|
||||
let error = $state<string | null>(null);
|
||||
let loading = $state(false);
|
||||
let success = $state(false);
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = null;
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = null;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
error = 'Die Passwörter stimmen nicht überein';
|
||||
return;
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
error = 'Die Passwörter stimmen nicht überein';
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
error = 'Das Passwort muss mindestens 6 Zeichen lang sein';
|
||||
return;
|
||||
}
|
||||
if (password.length < 6) {
|
||||
error = 'Das Passwort muss mindestens 6 Zeichen lang sein';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
loading = true;
|
||||
|
||||
const result = await authStore.signUp(email, password);
|
||||
const result = await authStore.signUp(email, password);
|
||||
|
||||
if (result.success) {
|
||||
if (result.needsVerification) {
|
||||
success = true;
|
||||
} else {
|
||||
goto('/');
|
||||
}
|
||||
} else {
|
||||
error = result.error || 'Registrierung fehlgeschlagen';
|
||||
}
|
||||
if (result.success) {
|
||||
if (result.needsVerification) {
|
||||
success = true;
|
||||
} else {
|
||||
goto('/');
|
||||
}
|
||||
} else {
|
||||
error = result.error || 'Registrierung fehlgeschlagen';
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Registrieren | ManaChat</title>
|
||||
<title>Registrieren | ManaChat</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">ManaChat</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-2">Erstelle dein Konto</p>
|
||||
</div>
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">ManaChat</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-2">Erstelle dein Konto</p>
|
||||
</div>
|
||||
|
||||
{#if success}
|
||||
<div class="p-4 bg-green-100 dark:bg-green-900/30 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<p class="text-green-700 dark:text-green-400 text-center">
|
||||
Bitte überprüfe deine E-Mails, um dein Konto zu bestätigen.
|
||||
</p>
|
||||
<div class="mt-4 text-center">
|
||||
<a href="/login" class="text-blue-600 dark:text-blue-400 hover:underline font-medium">
|
||||
Zur Anmeldung
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if error}
|
||||
<div class="mb-4 p-3 bg-red-100 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
{#if success}
|
||||
<div
|
||||
class="p-4 bg-green-100 dark:bg-green-900/30 border border-green-200 dark:border-green-800 rounded-lg"
|
||||
>
|
||||
<p class="text-green-700 dark:text-green-400 text-center">
|
||||
Bitte überprüfe deine E-Mails, um dein Konto zu bestätigen.
|
||||
</p>
|
||||
<div class="mt-4 text-center">
|
||||
<a href="/login" class="text-blue-600 dark:text-blue-400 hover:underline font-medium">
|
||||
Zur Anmeldung
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if error}
|
||||
<div
|
||||
class="mb-4 p-3 bg-red-100 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400 text-sm"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
E-Mail
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
bind:value={email}
|
||||
required
|
||||
disabled={loading}
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
E-Mail
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
bind:value={email}
|
||||
required
|
||||
disabled={loading}
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
placeholder="deine@email.de"
|
||||
/>
|
||||
</div>
|
||||
placeholder="deine@email.de"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Passwort
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
bind:value={password}
|
||||
required
|
||||
disabled={loading}
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600
|
||||
<div>
|
||||
<label
|
||||
for="password"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Passwort
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
bind:value={password}
|
||||
required
|
||||
disabled={loading}
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Passwort bestätigen
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
bind:value={confirmPassword}
|
||||
required
|
||||
disabled={loading}
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600
|
||||
<div>
|
||||
<label
|
||||
for="confirmPassword"
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Passwort bestätigen
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
bind:value={confirmPassword}
|
||||
required
|
||||
disabled={loading}
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg
|
||||
transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{#if loading}
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Wird registriert...
|
||||
</span>
|
||||
{:else}
|
||||
Registrieren
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
>
|
||||
{#if loading}
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
Wird registriert...
|
||||
</span>
|
||||
{:else}
|
||||
Registrieren
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Bereits ein Konto?
|
||||
<a href="/login" class="text-blue-600 dark:text-blue-400 hover:underline font-medium">
|
||||
Jetzt anmelden
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Bereits ein Konto?
|
||||
<a href="/login" class="text-blue-600 dark:text-blue-400 hover:underline font-medium">
|
||||
Jetzt anmelden
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ url }) => {
|
||||
// Return the current path for client-side redirect logic
|
||||
return {
|
||||
pathname: url.pathname,
|
||||
};
|
||||
// Return the current path for client-side redirect logic
|
||||
return {
|
||||
pathname: url.pathname,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,127 +1,132 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
|
||||
let { children, data }: { children: any; data: LayoutData } = $props();
|
||||
let { children, data }: { children: any; data: LayoutData } = $props();
|
||||
|
||||
let isChecking = $state(true);
|
||||
let isChecking = $state(true);
|
||||
|
||||
// Check auth on mount and redirect if not authenticated
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
// Check auth on mount and redirect if not authenticated
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
|
||||
if (!authStore.isAuthenticated) {
|
||||
const redirectTo = encodeURIComponent(data.pathname || '/chat');
|
||||
goto(`/login?redirectTo=${redirectTo}`);
|
||||
return;
|
||||
}
|
||||
if (!authStore.isAuthenticated) {
|
||||
const redirectTo = encodeURIComponent(data.pathname || '/chat');
|
||||
goto(`/login?redirectTo=${redirectTo}`);
|
||||
return;
|
||||
}
|
||||
|
||||
isChecking = false;
|
||||
});
|
||||
isChecking = false;
|
||||
});
|
||||
|
||||
async function handleSignOut() {
|
||||
await authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
async function handleSignOut() {
|
||||
await authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isChecking}
|
||||
<!-- Loading state while checking auth -->
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 dark:border-white"></div>
|
||||
</div>
|
||||
<!-- Loading state while checking auth -->
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
|
||||
<div
|
||||
class="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 dark:border-white"
|
||||
></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<!-- Top Navigation -->
|
||||
<nav class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="text-xl font-bold text-gray-900 dark:text-white">
|
||||
ManaChat
|
||||
</a>
|
||||
<div class="hidden sm:ml-8 sm:flex sm:space-x-4">
|
||||
<a
|
||||
href="/"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<!-- Top Navigation -->
|
||||
<nav class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="text-xl font-bold text-gray-900 dark:text-white"> ManaChat </a>
|
||||
<div class="hidden sm:ml-8 sm:flex sm:space-x-4">
|
||||
<a
|
||||
href="/"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname === '/' || $page.url.pathname.startsWith('/chat')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Chat
|
||||
</a>
|
||||
<a
|
||||
href="/templates"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Chat
|
||||
</a>
|
||||
<a
|
||||
href="/templates"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname.startsWith('/templates')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Templates
|
||||
</a>
|
||||
<a
|
||||
href="/spaces"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Templates
|
||||
</a>
|
||||
<a
|
||||
href="/spaces"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname.startsWith('/spaces')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Spaces
|
||||
</a>
|
||||
<a
|
||||
href="/documents"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Spaces
|
||||
</a>
|
||||
<a
|
||||
href="/documents"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname.startsWith('/documents')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Dokumente
|
||||
</a>
|
||||
<a
|
||||
href="/archive"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Dokumente
|
||||
</a>
|
||||
<a
|
||||
href="/archive"
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
{$page.url.pathname.startsWith('/archive')
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Archiv
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
Archiv
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
{#if authStore.user}
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 hidden sm:block">
|
||||
{authStore.user.email}
|
||||
</span>
|
||||
{/if}
|
||||
<a
|
||||
href="/profile"
|
||||
class="p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
aria-label="Profil"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</a>
|
||||
<button
|
||||
onclick={handleSignOut}
|
||||
class="px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-300
|
||||
<div class="flex items-center gap-4">
|
||||
{#if authStore.user}
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 hidden sm:block">
|
||||
{authStore.user.email}
|
||||
</span>
|
||||
{/if}
|
||||
<a
|
||||
href="/profile"
|
||||
class="p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
aria-label="Profil"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<button
|
||||
onclick={handleSignOut}
|
||||
class="px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-300
|
||||
hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,166 +1,184 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import type { Conversation } from '@chat/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import type { Conversation } from '@chat/types';
|
||||
|
||||
let conversations = $state<Conversation[]>([]);
|
||||
let isLoading = $state(true);
|
||||
let conversations = $state<Conversation[]>([]);
|
||||
let isLoading = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
if (authStore.user) {
|
||||
await conversationsStore.loadArchivedConversations(authStore.user.id);
|
||||
conversations = conversationsStore.archivedConversations;
|
||||
}
|
||||
isLoading = false;
|
||||
});
|
||||
onMount(async () => {
|
||||
if (authStore.user) {
|
||||
await conversationsStore.loadArchivedConversations(authStore.user.id);
|
||||
conversations = conversationsStore.archivedConversations;
|
||||
}
|
||||
isLoading = false;
|
||||
});
|
||||
|
||||
// Keep conversations in sync with store
|
||||
$effect(() => {
|
||||
conversations = conversationsStore.archivedConversations;
|
||||
});
|
||||
// Keep conversations in sync with store
|
||||
$effect(() => {
|
||||
conversations = conversationsStore.archivedConversations;
|
||||
});
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function handleConversationClick(id: string) {
|
||||
goto(`/chat/${id}`);
|
||||
}
|
||||
function handleConversationClick(id: string) {
|
||||
goto(`/chat/${id}`);
|
||||
}
|
||||
|
||||
async function handleUnarchive(id: string) {
|
||||
await conversationsStore.unarchiveConversation(id);
|
||||
}
|
||||
async function handleUnarchive(id: string) {
|
||||
await conversationsStore.unarchiveConversation(id);
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (confirm('Möchtest du diese Konversation wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) {
|
||||
await conversationsStore.deleteConversation(id);
|
||||
}
|
||||
}
|
||||
async function handleDelete(id: string) {
|
||||
if (
|
||||
confirm(
|
||||
'Möchtest du diese Konversation wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.'
|
||||
)
|
||||
) {
|
||||
await conversationsStore.deleteConversation(id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Archiv | ManaChat</title>
|
||||
<title>Archiv | ManaChat</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-[calc(100vh-4rem)] bg-gray-50 dark:bg-gray-900 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Archiv</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Deine archivierten Konversationen.
|
||||
</p>
|
||||
</div>
|
||||
<div class="max-w-4xl mx-auto px-4">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Archiv</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Deine archivierten Konversationen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
{#if isLoading}
|
||||
<div class="flex items-center justify-center py-16">
|
||||
<div class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"></div>
|
||||
</div>
|
||||
{:else if conversations.length === 0}
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-16">
|
||||
<svg
|
||||
class="w-16 h-16 text-gray-400 mx-auto mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-1">Keine archivierten Konversationen</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
Archivierte Gespräche erscheinen hier.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Conversation List -->
|
||||
<div class="space-y-3">
|
||||
{#each conversations as conv (conv.id)}
|
||||
<div
|
||||
class="group bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700
|
||||
<!-- Loading State -->
|
||||
{#if isLoading}
|
||||
<div class="flex items-center justify-center py-16">
|
||||
<div
|
||||
class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
{:else if conversations.length === 0}
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-16">
|
||||
<svg
|
||||
class="w-16 h-16 text-gray-400 mx-auto mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
||||
Keine archivierten Konversationen
|
||||
</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">Archivierte Gespräche erscheinen hier.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Conversation List -->
|
||||
<div class="space-y-3">
|
||||
{#each conversations as conv (conv.id)}
|
||||
<div
|
||||
class="group bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700
|
||||
shadow-sm hover:shadow-md transition-all overflow-hidden"
|
||||
>
|
||||
<button
|
||||
onclick={() => handleConversationClick(conv.id)}
|
||||
class="w-full p-4 text-left"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">
|
||||
{conv.title || 'Unbenannte Konversation'}
|
||||
</h3>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">{formatDate(conv.updated_at)}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||
<span class="px-2 py-0.5 bg-gray-100 dark:bg-gray-700 rounded">
|
||||
{conv.conversation_mode === 'free' ? 'Freier Modus' :
|
||||
conv.conversation_mode === 'guided' ? 'Geführter Modus' : 'Vorlagen-Modus'}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
>
|
||||
<button onclick={() => handleConversationClick(conv.id)} class="w-full p-4 text-left">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
class="w-5 h-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">
|
||||
{conv.title || 'Unbenannte Konversation'}
|
||||
</h3>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">{formatDate(conv.updated_at)}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||
<span class="px-2 py-0.5 bg-gray-100 dark:bg-gray-700 rounded">
|
||||
{conv.conversation_mode === 'free'
|
||||
? 'Freier Modus'
|
||||
: conv.conversation_mode === 'guided'
|
||||
? 'Geführter Modus'
|
||||
: 'Vorlagen-Modus'}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end gap-2 px-4 py-2 border-t border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
|
||||
<button
|
||||
onclick={() => handleUnarchive(conv.id)}
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400
|
||||
<!-- Actions -->
|
||||
<div
|
||||
class="flex justify-end gap-2 px-4 py-2 border-t border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50"
|
||||
>
|
||||
<button
|
||||
onclick={() => handleUnarchive(conv.id)}
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400
|
||||
hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20
|
||||
rounded-lg transition-colors"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
||||
</svg>
|
||||
Wiederherstellen
|
||||
</button>
|
||||
<button
|
||||
onclick={() => handleDelete(conv.id)}
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"
|
||||
/>
|
||||
</svg>
|
||||
Wiederherstellen
|
||||
</button>
|
||||
<button
|
||||
onclick={() => handleDelete(conv.id)}
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400
|
||||
hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20
|
||||
rounded-lg transition-colors"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if conversationsStore.error}
|
||||
<div class="mt-4 p-4 bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-400 rounded-lg">
|
||||
{conversationsStore.error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Error Message -->
|
||||
{#if conversationsStore.error}
|
||||
<div class="mt-4 p-4 bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-400 rounded-lg">
|
||||
{conversationsStore.error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,66 +1,78 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import ConversationList from '$lib/components/chat/ConversationList.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import ConversationList from '$lib/components/chat/ConversationList.svelte';
|
||||
|
||||
let { children }: { children: any } = $props();
|
||||
let showSidebar = $state(true);
|
||||
let { children }: { children: any } = $props();
|
||||
let showSidebar = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
if (authStore.user) {
|
||||
await conversationsStore.loadConversations(authStore.user.id);
|
||||
}
|
||||
});
|
||||
onMount(async () => {
|
||||
if (authStore.user) {
|
||||
await conversationsStore.loadConversations(authStore.user.id);
|
||||
}
|
||||
});
|
||||
|
||||
function toggleSidebar() {
|
||||
showSidebar = !showSidebar;
|
||||
}
|
||||
function toggleSidebar() {
|
||||
showSidebar = !showSidebar;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-[calc(100vh-4rem)]">
|
||||
<!-- Sidebar Toggle (mobile) -->
|
||||
<button
|
||||
onclick={toggleSidebar}
|
||||
class="fixed bottom-4 left-4 z-50 p-3 bg-blue-600 text-white rounded-full shadow-lg
|
||||
<!-- Sidebar Toggle (mobile) -->
|
||||
<button
|
||||
onclick={toggleSidebar}
|
||||
class="fixed bottom-4 left-4 z-50 p-3 bg-blue-600 text-white rounded-full shadow-lg
|
||||
sm:hidden hover:bg-blue-700 transition-colors"
|
||||
aria-label={showSidebar ? 'Seitenleiste schließen' : 'Seitenleiste öffnen'}
|
||||
>
|
||||
{#if showSidebar}
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
aria-label={showSidebar ? 'Seitenleiste schließen' : 'Seitenleiste öffnen'}
|
||||
>
|
||||
{#if showSidebar}
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
class="w-72 flex-shrink-0 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
class="w-72 flex-shrink-0 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700
|
||||
transition-transform duration-200 ease-in-out
|
||||
fixed sm:static inset-y-0 left-0 z-40 top-16
|
||||
{showSidebar ? 'translate-x-0' : '-translate-x-full sm:translate-x-0 sm:w-0 sm:border-0'}"
|
||||
>
|
||||
<ConversationList
|
||||
conversations={conversationsStore.conversations}
|
||||
isLoading={conversationsStore.isLoading}
|
||||
/>
|
||||
</aside>
|
||||
{showSidebar
|
||||
? 'translate-x-0'
|
||||
: '-translate-x-full sm:translate-x-0 sm:w-0 sm:border-0'}"
|
||||
>
|
||||
<ConversationList
|
||||
conversations={conversationsStore.conversations}
|
||||
isLoading={conversationsStore.isLoading}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<!-- Mobile Overlay -->
|
||||
{#if showSidebar}
|
||||
<button
|
||||
class="fixed inset-0 bg-black/50 z-30 sm:hidden"
|
||||
onclick={toggleSidebar}
|
||||
aria-label="Seitenleiste schließen"
|
||||
></button>
|
||||
{/if}
|
||||
<!-- Mobile Overlay -->
|
||||
{#if showSidebar}
|
||||
<button
|
||||
class="fixed inset-0 bg-black/50 z-30 sm:hidden"
|
||||
onclick={toggleSidebar}
|
||||
aria-label="Seitenleiste schließen"
|
||||
></button>
|
||||
{/if}
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 overflow-hidden">
|
||||
{@render children()}
|
||||
</main>
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 overflow-hidden">
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,227 +1,229 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { chatService } from '$lib/services/chat';
|
||||
import { conversationService } from '$lib/services/conversation';
|
||||
import { templateService } from '$lib/services/template';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import MessageList from '$lib/components/chat/MessageList.svelte';
|
||||
import ChatInput from '$lib/components/chat/ChatInput.svelte';
|
||||
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import type { AIModel, Message, Template } from '@chat/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { chatService } from '$lib/services/chat';
|
||||
import { conversationService } from '$lib/services/conversation';
|
||||
import { templateService } from '$lib/services/template';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import MessageList from '$lib/components/chat/MessageList.svelte';
|
||||
import ChatInput from '$lib/components/chat/ChatInput.svelte';
|
||||
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import type { AIModel, Message, Template } from '@chat/types';
|
||||
|
||||
let models = $state<AIModel[]>([]);
|
||||
let templates = $state<Template[]>([]);
|
||||
let selectedModelId = $state('');
|
||||
let selectedTemplateId = $state('');
|
||||
let documentMode = $state(false);
|
||||
let messages = $state<Message[]>([]);
|
||||
let isLoading = $state(true);
|
||||
let isSending = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let models = $state<AIModel[]>([]);
|
||||
let templates = $state<Template[]>([]);
|
||||
let selectedModelId = $state('');
|
||||
let selectedTemplateId = $state('');
|
||||
let documentMode = $state(false);
|
||||
let messages = $state<Message[]>([]);
|
||||
let isLoading = $state(true);
|
||||
let isSending = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Get selected template
|
||||
const selectedTemplate = $derived(templates.find((t) => t.id === selectedTemplateId));
|
||||
// Get selected template
|
||||
const selectedTemplate = $derived(templates.find((t) => t.id === selectedTemplateId));
|
||||
|
||||
onMount(async () => {
|
||||
models = await chatService.getModels();
|
||||
if (models.length > 0) {
|
||||
selectedModelId = models[0].id;
|
||||
}
|
||||
onMount(async () => {
|
||||
models = await chatService.getModels();
|
||||
if (models.length > 0) {
|
||||
selectedModelId = models[0].id;
|
||||
}
|
||||
|
||||
// Load user templates
|
||||
if (authStore.user) {
|
||||
templates = await templateService.getTemplates(authStore.user.id);
|
||||
}
|
||||
// Load user templates
|
||||
if (authStore.user) {
|
||||
templates = await templateService.getTemplates(authStore.user.id);
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
});
|
||||
isLoading = false;
|
||||
});
|
||||
|
||||
async function handleSend(text: string) {
|
||||
if (!authStore.user || !selectedModelId) return;
|
||||
async function handleSend(text: string) {
|
||||
if (!authStore.user || !selectedModelId) return;
|
||||
|
||||
isSending = true;
|
||||
error = null;
|
||||
isSending = true;
|
||||
error = null;
|
||||
|
||||
// Add optimistic user message
|
||||
const tempUserMessage: Message = {
|
||||
id: `temp-${Date.now()}`,
|
||||
conversation_id: '',
|
||||
sender: 'user',
|
||||
message_text: text,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
messages = [...messages, tempUserMessage];
|
||||
// Add optimistic user message
|
||||
const tempUserMessage: Message = {
|
||||
id: `temp-${Date.now()}`,
|
||||
conversation_id: '',
|
||||
sender: 'user',
|
||||
message_text: text,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
messages = [...messages, tempUserMessage];
|
||||
|
||||
try {
|
||||
// Determine mode and model based on template
|
||||
const mode = selectedTemplate ? 'template' : 'free';
|
||||
const modelToUse = selectedTemplate?.model_id || selectedModelId;
|
||||
const docMode = selectedTemplate?.document_mode || documentMode;
|
||||
try {
|
||||
// Determine mode and model based on template
|
||||
const mode = selectedTemplate ? 'template' : 'free';
|
||||
const modelToUse = selectedTemplate?.model_id || selectedModelId;
|
||||
const docMode = selectedTemplate?.document_mode || documentMode;
|
||||
|
||||
// Create new conversation
|
||||
const conversationId = await conversationService.createConversation(
|
||||
authStore.user.id,
|
||||
modelToUse,
|
||||
mode as 'free' | 'guided' | 'template',
|
||||
selectedTemplate?.id,
|
||||
docMode
|
||||
);
|
||||
// Create new conversation
|
||||
const conversationId = await conversationService.createConversation(
|
||||
authStore.user.id,
|
||||
modelToUse,
|
||||
mode as 'free' | 'guided' | 'template',
|
||||
selectedTemplate?.id,
|
||||
docMode
|
||||
);
|
||||
|
||||
if (!conversationId) {
|
||||
throw new Error('Konversation konnte nicht erstellt werden');
|
||||
}
|
||||
if (!conversationId) {
|
||||
throw new Error('Konversation konnte nicht erstellt werden');
|
||||
}
|
||||
|
||||
// Send message and get response
|
||||
const result = await conversationService.sendMessageAndGetResponse(
|
||||
conversationId,
|
||||
text,
|
||||
modelToUse
|
||||
);
|
||||
// Send message and get response
|
||||
const result = await conversationService.sendMessageAndGetResponse(
|
||||
conversationId,
|
||||
text,
|
||||
modelToUse
|
||||
);
|
||||
|
||||
// Reload conversations list
|
||||
await conversationsStore.loadConversations(authStore.user.id);
|
||||
// Reload conversations list
|
||||
await conversationsStore.loadConversations(authStore.user.id);
|
||||
|
||||
// Navigate to the new conversation
|
||||
goto(`/chat/${conversationId}`);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Erstellen der Konversation';
|
||||
messages = [];
|
||||
} finally {
|
||||
isSending = false;
|
||||
}
|
||||
}
|
||||
// Navigate to the new conversation
|
||||
goto(`/chat/${conversationId}`);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Erstellen der Konversation';
|
||||
messages = [];
|
||||
} finally {
|
||||
isSending = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleModelSelect(modelId: string) {
|
||||
selectedModelId = modelId;
|
||||
}
|
||||
function handleModelSelect(modelId: string) {
|
||||
selectedModelId = modelId;
|
||||
}
|
||||
|
||||
function handleTemplateSelect(e: Event) {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
selectedTemplateId = target.value;
|
||||
function handleTemplateSelect(e: Event) {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
selectedTemplateId = target.value;
|
||||
|
||||
// If template has a model, update selected model
|
||||
const template = templates.find((t) => t.id === target.value);
|
||||
if (template?.model_id) {
|
||||
selectedModelId = template.model_id;
|
||||
}
|
||||
// If template has document mode, enable it
|
||||
if (template?.document_mode) {
|
||||
documentMode = true;
|
||||
}
|
||||
}
|
||||
// If template has a model, update selected model
|
||||
const template = templates.find((t) => t.id === target.value);
|
||||
if (template?.model_id) {
|
||||
selectedModelId = template.model_id;
|
||||
}
|
||||
// If template has document mode, enable it
|
||||
if (template?.document_mode) {
|
||||
documentMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDocumentMode() {
|
||||
documentMode = !documentMode;
|
||||
}
|
||||
function toggleDocumentMode() {
|
||||
documentMode = !documentMode;
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
theme.toggleMode();
|
||||
}
|
||||
function toggleTheme() {
|
||||
theme.toggleMode();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Chat | ManaChat</title>
|
||||
<title>Chat | ManaChat</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Chat Header -->
|
||||
<header class="flex-shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 py-3">
|
||||
<div class="flex items-center justify-between max-w-4xl mx-auto">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Neuer Chat</h2>
|
||||
<!-- Chat Header -->
|
||||
<header
|
||||
class="flex-shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 py-3"
|
||||
>
|
||||
<div class="flex items-center justify-between max-w-4xl mx-auto">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Neuer Chat</h2>
|
||||
|
||||
<!-- Model Selector -->
|
||||
<ModelSelector
|
||||
{models}
|
||||
{selectedModelId}
|
||||
onSelect={handleModelSelect}
|
||||
disabled={isSending}
|
||||
/>
|
||||
<!-- Model Selector -->
|
||||
<ModelSelector
|
||||
{models}
|
||||
{selectedModelId}
|
||||
onSelect={handleModelSelect}
|
||||
disabled={isSending}
|
||||
/>
|
||||
|
||||
<!-- Template Selector -->
|
||||
{#if templates.length > 0}
|
||||
<select
|
||||
onchange={handleTemplateSelect}
|
||||
value={selectedTemplateId}
|
||||
disabled={isSending}
|
||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
<!-- Template Selector -->
|
||||
{#if templates.length > 0}
|
||||
<select
|
||||
onchange={handleTemplateSelect}
|
||||
value={selectedTemplateId}
|
||||
disabled={isSending}
|
||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-white dark:bg-gray-800 text-gray-900 dark:text-white
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
||||
disabled:opacity-50"
|
||||
>
|
||||
<option value="">Ohne Vorlage</option>
|
||||
{#each templates as template}
|
||||
<option value={template.id}>
|
||||
{template.name}
|
||||
{template.is_default ? ' (Standard)' : ''}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
>
|
||||
<option value="">Ohne Vorlage</option>
|
||||
{#each templates as template}
|
||||
<option value={template.id}>
|
||||
{template.name}
|
||||
{template.is_default ? ' (Standard)' : ''}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
|
||||
<!-- Document Mode Toggle -->
|
||||
<button
|
||||
onclick={toggleDocumentMode}
|
||||
disabled={isSending}
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg transition-colors
|
||||
<!-- Document Mode Toggle -->
|
||||
<button
|
||||
onclick={toggleDocumentMode}
|
||||
disabled={isSending}
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg transition-colors
|
||||
{documentMode
|
||||
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-300 dark:border-blue-700'
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border border-gray-300 dark:border-gray-600'}
|
||||
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-300 dark:border-blue-700'
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border border-gray-300 dark:border-gray-600'}
|
||||
hover:bg-opacity-80 disabled:opacity-50"
|
||||
title="Dokumentmodus aktivieren"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
Dokument
|
||||
</button>
|
||||
</div>
|
||||
title="Dokumentmodus aktivieren"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
Dokument
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick={toggleTheme}
|
||||
class="p-2 text-gray-700 dark:text-gray-300
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick={toggleTheme}
|
||||
class="p-2 text-gray-700 dark:text-gray-300
|
||||
bg-gray-100 dark:bg-gray-800 rounded-lg
|
||||
hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
||||
aria-label="Theme wechseln"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
aria-label="Theme wechseln"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Messages Area -->
|
||||
<main class="flex-1 overflow-hidden bg-white dark:bg-gray-900">
|
||||
<div class="h-full max-w-4xl mx-auto flex flex-col">
|
||||
<MessageList {messages} isTyping={isSending} />
|
||||
</div>
|
||||
</main>
|
||||
<!-- Messages Area -->
|
||||
<main class="flex-1 overflow-hidden bg-white dark:bg-gray-900">
|
||||
<div class="h-full max-w-4xl mx-auto flex flex-col">
|
||||
<MessageList {messages} isTyping={isSending} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Input Area -->
|
||||
<ChatInput onSend={handleSend} disabled={isSending || isLoading} />
|
||||
<!-- Input Area -->
|
||||
<ChatInput onSend={handleSend} disabled={isSending || isLoading} />
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<div
|
||||
class="fixed bottom-24 left-1/2 -translate-x-1/2 px-4 py-2 bg-red-500 text-white rounded-lg shadow-lg"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<div
|
||||
class="fixed bottom-24 left-1/2 -translate-x-1/2 px-4 py-2 bg-red-500 text-white rounded-lg shadow-lg"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,403 +1,472 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { conversationService } from '$lib/services/conversation';
|
||||
import { chatService } from '$lib/services/chat';
|
||||
import { documentService } from '$lib/services/document';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import MessageList from '$lib/components/chat/MessageList.svelte';
|
||||
import ChatInput from '$lib/components/chat/ChatInput.svelte';
|
||||
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
|
||||
import type { Conversation, Message, AIModel, Document } from '@chat/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { conversationService } from '$lib/services/conversation';
|
||||
import { chatService } from '$lib/services/chat';
|
||||
import { documentService } from '$lib/services/document';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import MessageList from '$lib/components/chat/MessageList.svelte';
|
||||
import ChatInput from '$lib/components/chat/ChatInput.svelte';
|
||||
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
|
||||
import type { Conversation, Message, AIModel, Document } from '@chat/types';
|
||||
|
||||
let conversation = $state<Conversation | null>(null);
|
||||
let messages = $state<Message[]>([]);
|
||||
let models = $state<AIModel[]>([]);
|
||||
let selectedModelId = $state('');
|
||||
let isLoading = $state(true);
|
||||
let isSending = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let conversation = $state<Conversation | null>(null);
|
||||
let messages = $state<Message[]>([]);
|
||||
let models = $state<AIModel[]>([]);
|
||||
let selectedModelId = $state('');
|
||||
let isLoading = $state(true);
|
||||
let isSending = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Document mode state
|
||||
let document = $state<Document | null>(null);
|
||||
let documentContent = $state('');
|
||||
let documentVersions = $state<Document[]>([]);
|
||||
let isSavingDocument = $state(false);
|
||||
let showVersionsModal = $state(false);
|
||||
let showDocumentPanel = $state(true);
|
||||
// Document mode state
|
||||
let document = $state<Document | null>(null);
|
||||
let documentContent = $state('');
|
||||
let documentVersions = $state<Document[]>([]);
|
||||
let isSavingDocument = $state(false);
|
||||
let showVersionsModal = $state(false);
|
||||
let showDocumentPanel = $state(true);
|
||||
|
||||
const conversationId = $derived($page.params.id ?? '');
|
||||
const isDocumentMode = $derived(conversation?.document_mode ?? false);
|
||||
const conversationId = $derived($page.params.id ?? '');
|
||||
const isDocumentMode = $derived(conversation?.document_mode ?? false);
|
||||
|
||||
onMount(async () => {
|
||||
await loadData();
|
||||
});
|
||||
onMount(async () => {
|
||||
await loadData();
|
||||
});
|
||||
|
||||
async function loadData() {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
async function loadData() {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Load models
|
||||
models = await chatService.getModels();
|
||||
try {
|
||||
// Load models
|
||||
models = await chatService.getModels();
|
||||
|
||||
// Load conversation
|
||||
conversation = await conversationService.getConversation(conversationId);
|
||||
// Load conversation
|
||||
conversation = await conversationService.getConversation(conversationId);
|
||||
|
||||
if (!conversation) {
|
||||
error = 'Konversation nicht gefunden';
|
||||
return;
|
||||
}
|
||||
if (!conversation) {
|
||||
error = 'Konversation nicht gefunden';
|
||||
return;
|
||||
}
|
||||
|
||||
// Set model from conversation
|
||||
selectedModelId = conversation.model_id;
|
||||
// Set model from conversation
|
||||
selectedModelId = conversation.model_id;
|
||||
|
||||
// Load messages
|
||||
messages = await conversationService.getMessages(conversationId);
|
||||
// Load messages
|
||||
messages = await conversationService.getMessages(conversationId);
|
||||
|
||||
// Load document if in document mode
|
||||
if (conversation.document_mode) {
|
||||
document = await documentService.getLatestDocument(conversationId);
|
||||
documentContent = document?.content ?? '';
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Laden';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
// Load document if in document mode
|
||||
if (conversation.document_mode) {
|
||||
document = await documentService.getLatestDocument(conversationId);
|
||||
documentContent = document?.content ?? '';
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Laden';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDocument() {
|
||||
if (!documentContent.trim()) return;
|
||||
async function saveDocument() {
|
||||
if (!documentContent.trim()) return;
|
||||
|
||||
isSavingDocument = true;
|
||||
try {
|
||||
if (document) {
|
||||
// Create new version
|
||||
document = await documentService.createDocumentVersion(conversationId, documentContent);
|
||||
} else {
|
||||
// Create first document
|
||||
document = await documentService.createDocument(conversationId, documentContent);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Speichern';
|
||||
} finally {
|
||||
isSavingDocument = false;
|
||||
}
|
||||
}
|
||||
isSavingDocument = true;
|
||||
try {
|
||||
if (document) {
|
||||
// Create new version
|
||||
document = await documentService.createDocumentVersion(conversationId, documentContent);
|
||||
} else {
|
||||
// Create first document
|
||||
document = await documentService.createDocument(conversationId, documentContent);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Speichern';
|
||||
} finally {
|
||||
isSavingDocument = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVersions() {
|
||||
documentVersions = await documentService.getAllDocumentVersions(conversationId);
|
||||
showVersionsModal = true;
|
||||
}
|
||||
async function loadVersions() {
|
||||
documentVersions = await documentService.getAllDocumentVersions(conversationId);
|
||||
showVersionsModal = true;
|
||||
}
|
||||
|
||||
function restoreVersion(version: Document) {
|
||||
documentContent = version.content;
|
||||
showVersionsModal = false;
|
||||
}
|
||||
function restoreVersion(version: Document) {
|
||||
documentContent = version.content;
|
||||
showVersionsModal = false;
|
||||
}
|
||||
|
||||
function toggleDocumentPanel() {
|
||||
showDocumentPanel = !showDocumentPanel;
|
||||
}
|
||||
function toggleDocumentPanel() {
|
||||
showDocumentPanel = !showDocumentPanel;
|
||||
}
|
||||
|
||||
async function handleSend(text: string) {
|
||||
if (!conversation || !selectedModelId) return;
|
||||
async function handleSend(text: string) {
|
||||
if (!conversation || !selectedModelId) return;
|
||||
|
||||
isSending = true;
|
||||
error = null;
|
||||
isSending = true;
|
||||
error = null;
|
||||
|
||||
// Optimistic update - add user message
|
||||
const tempUserMessage: Message = {
|
||||
id: `temp-user-${Date.now()}`,
|
||||
conversation_id: conversationId,
|
||||
sender: 'user',
|
||||
message_text: text,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
messages = [...messages, tempUserMessage];
|
||||
// Optimistic update - add user message
|
||||
const tempUserMessage: Message = {
|
||||
id: `temp-user-${Date.now()}`,
|
||||
conversation_id: conversationId,
|
||||
sender: 'user',
|
||||
message_text: text,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
messages = [...messages, tempUserMessage];
|
||||
|
||||
try {
|
||||
const result = await conversationService.sendMessageAndGetResponse(
|
||||
conversationId,
|
||||
text,
|
||||
selectedModelId
|
||||
);
|
||||
try {
|
||||
const result = await conversationService.sendMessageAndGetResponse(
|
||||
conversationId,
|
||||
text,
|
||||
selectedModelId
|
||||
);
|
||||
|
||||
// Update messages with real data
|
||||
messages = await conversationService.getMessages(conversationId);
|
||||
// Update messages with real data
|
||||
messages = await conversationService.getMessages(conversationId);
|
||||
|
||||
// Update conversation title if generated
|
||||
if (result.title && conversation) {
|
||||
conversation = { ...conversation, title: result.title };
|
||||
conversationsStore.updateConversation(conversationId, { title: result.title });
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Senden';
|
||||
// Remove optimistic message on error
|
||||
messages = messages.filter((m) => m.id !== tempUserMessage.id);
|
||||
} finally {
|
||||
isSending = false;
|
||||
}
|
||||
}
|
||||
// Update conversation title if generated
|
||||
if (result.title && conversation) {
|
||||
conversation = { ...conversation, title: result.title };
|
||||
conversationsStore.updateConversation(conversationId, { title: result.title });
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Senden';
|
||||
// Remove optimistic message on error
|
||||
messages = messages.filter((m) => m.id !== tempUserMessage.id);
|
||||
} finally {
|
||||
isSending = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleModelSelect(modelId: string) {
|
||||
selectedModelId = modelId;
|
||||
}
|
||||
function handleModelSelect(modelId: string) {
|
||||
selectedModelId = modelId;
|
||||
}
|
||||
|
||||
async function handleArchive() {
|
||||
if (!conversation) return;
|
||||
async function handleArchive() {
|
||||
if (!conversation) return;
|
||||
|
||||
const success = await conversationsStore.archiveConversation(conversationId);
|
||||
if (success) {
|
||||
goto('/chat');
|
||||
}
|
||||
}
|
||||
const success = await conversationsStore.archiveConversation(conversationId);
|
||||
if (success) {
|
||||
goto('/chat');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!conversation) return;
|
||||
async function handleDelete() {
|
||||
if (!conversation) return;
|
||||
|
||||
if (confirm('Möchtest du diese Konversation wirklich löschen?')) {
|
||||
const success = await conversationsStore.deleteConversation(conversationId);
|
||||
if (success) {
|
||||
goto('/chat');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (confirm('Möchtest du diese Konversation wirklich löschen?')) {
|
||||
const success = await conversationsStore.deleteConversation(conversationId);
|
||||
if (success) {
|
||||
goto('/chat');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
theme.toggleMode();
|
||||
}
|
||||
function toggleTheme() {
|
||||
theme.toggleMode();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{conversation?.title || 'Chat'} | ManaChat</title>
|
||||
<title>{conversation?.title || 'Chat'} | ManaChat</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<div class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"></div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<div
|
||||
class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
{:else if error && !conversation}
|
||||
<div class="flex flex-col items-center justify-center h-full text-center p-4">
|
||||
<p class="text-red-500 mb-4">{error}</p>
|
||||
<a href="/chat" class="text-blue-600 hover:underline">Zurück zum Chat</a>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center h-full text-center p-4">
|
||||
<p class="text-red-500 mb-4">{error}</p>
|
||||
<a href="/chat" class="text-blue-600 hover:underline">Zurück zum Chat</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Chat Header -->
|
||||
<header class="flex-shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 py-3">
|
||||
<div class="flex items-center justify-between max-w-4xl mx-auto">
|
||||
<div class="flex items-center gap-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white truncate max-w-xs">
|
||||
{conversation?.title || 'Chat'}
|
||||
</h2>
|
||||
<ModelSelector
|
||||
{models}
|
||||
{selectedModelId}
|
||||
onSelect={handleModelSelect}
|
||||
disabled={isSending}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if isDocumentMode}
|
||||
<button
|
||||
onclick={toggleDocumentPanel}
|
||||
class="p-2 transition-colors rounded-lg
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Chat Header -->
|
||||
<header
|
||||
class="flex-shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 py-3"
|
||||
>
|
||||
<div class="flex items-center justify-between max-w-4xl mx-auto">
|
||||
<div class="flex items-center gap-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white truncate max-w-xs">
|
||||
{conversation?.title || 'Chat'}
|
||||
</h2>
|
||||
<ModelSelector
|
||||
{models}
|
||||
{selectedModelId}
|
||||
onSelect={handleModelSelect}
|
||||
disabled={isSending}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if isDocumentMode}
|
||||
<button
|
||||
onclick={toggleDocumentPanel}
|
||||
class="p-2 transition-colors rounded-lg
|
||||
{showDocumentPanel
|
||||
? 'text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/30'
|
||||
: 'text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700'}"
|
||||
aria-label="Dokument-Panel"
|
||||
title="Dokument-Panel ein/ausblenden"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
onclick={handleArchive}
|
||||
class="p-2 text-gray-700 dark:text-gray-300
|
||||
? 'text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/30'
|
||||
: 'text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700'}"
|
||||
aria-label="Dokument-Panel"
|
||||
title="Dokument-Panel ein/ausblenden"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
onclick={handleArchive}
|
||||
class="p-2 text-gray-700 dark:text-gray-300
|
||||
bg-gray-100 dark:bg-gray-800 rounded-lg
|
||||
hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
||||
aria-label="Archivieren"
|
||||
title="Archivieren"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onclick={handleDelete}
|
||||
class="p-2 text-red-600 dark:text-red-400
|
||||
aria-label="Archivieren"
|
||||
title="Archivieren"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onclick={handleDelete}
|
||||
class="p-2 text-red-600 dark:text-red-400
|
||||
bg-gray-100 dark:bg-gray-800 rounded-lg
|
||||
hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors"
|
||||
aria-label="Löschen"
|
||||
title="Löschen"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onclick={toggleTheme}
|
||||
class="p-2 text-gray-700 dark:text-gray-300
|
||||
aria-label="Löschen"
|
||||
title="Löschen"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onclick={toggleTheme}
|
||||
class="p-2 text-gray-700 dark:text-gray-300
|
||||
bg-gray-100 dark:bg-gray-800 rounded-lg
|
||||
hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
||||
aria-label="Theme wechseln"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
aria-label="Theme wechseln"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- Chat Area -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden {isDocumentMode && showDocumentPanel ? 'lg:w-1/2' : 'w-full'}">
|
||||
<!-- Messages Area -->
|
||||
<main class="flex-1 overflow-hidden bg-white dark:bg-gray-900">
|
||||
<div class="h-full max-w-4xl mx-auto flex flex-col">
|
||||
<MessageList {messages} isTyping={isSending} />
|
||||
</div>
|
||||
</main>
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- Chat Area -->
|
||||
<div
|
||||
class="flex-1 flex flex-col overflow-hidden {isDocumentMode && showDocumentPanel
|
||||
? 'lg:w-1/2'
|
||||
: 'w-full'}"
|
||||
>
|
||||
<!-- Messages Area -->
|
||||
<main class="flex-1 overflow-hidden bg-white dark:bg-gray-900">
|
||||
<div class="h-full max-w-4xl mx-auto flex flex-col">
|
||||
<MessageList {messages} isTyping={isSending} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Input Area -->
|
||||
<ChatInput onSend={handleSend} disabled={isSending} />
|
||||
</div>
|
||||
<!-- Input Area -->
|
||||
<ChatInput onSend={handleSend} disabled={isSending} />
|
||||
</div>
|
||||
|
||||
<!-- Document Panel -->
|
||||
{#if isDocumentMode && showDocumentPanel}
|
||||
<div class="hidden lg:flex lg:w-1/2 flex-col border-l border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
|
||||
<!-- Document Header -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span class="font-medium text-gray-900 dark:text-white">Dokument</span>
|
||||
{#if document}
|
||||
<span class="text-xs text-gray-500 bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">
|
||||
v{document.version}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick={loadVersions}
|
||||
class="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white
|
||||
<!-- Document Panel -->
|
||||
{#if isDocumentMode && showDocumentPanel}
|
||||
<div
|
||||
class="hidden lg:flex lg:w-1/2 flex-col border-l border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900"
|
||||
>
|
||||
<!-- Document Header -->
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
class="w-5 h-5 text-blue-600 dark:text-blue-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-medium text-gray-900 dark:text-white">Dokument</span>
|
||||
{#if document}
|
||||
<span
|
||||
class="text-xs text-gray-500 bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded"
|
||||
>
|
||||
v{document.version}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick={loadVersions}
|
||||
class="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white
|
||||
hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors"
|
||||
title="Versionen anzeigen"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onclick={saveDocument}
|
||||
disabled={isSavingDocument || !documentContent.trim()}
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white
|
||||
title="Versionen anzeigen"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onclick={saveDocument}
|
||||
disabled={isSavingDocument || !documentContent.trim()}
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white
|
||||
bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400
|
||||
rounded-lg transition-colors"
|
||||
>
|
||||
{#if isSavingDocument}
|
||||
<div class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||
{:else}
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
||||
</svg>
|
||||
{/if}
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
>
|
||||
{#if isSavingDocument}
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"
|
||||
></div>
|
||||
{:else}
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Document Editor -->
|
||||
<div class="flex-1 p-4 overflow-auto">
|
||||
<textarea
|
||||
bind:value={documentContent}
|
||||
placeholder="Beginne hier mit dem Schreiben deines Dokuments...
|
||||
<!-- Document Editor -->
|
||||
<div class="flex-1 p-4 overflow-auto">
|
||||
<textarea
|
||||
bind:value={documentContent}
|
||||
placeholder="Beginne hier mit dem Schreiben deines Dokuments...
|
||||
|
||||
Du kannst Markdown verwenden:
|
||||
# Überschrift
|
||||
## Unterüberschrift
|
||||
- Aufzählung
|
||||
**Fett** und *Kursiv*"
|
||||
class="w-full h-full min-h-[300px] p-4 text-sm font-mono
|
||||
class="w-full h-full min-h-[300px] p-4 text-sm font-mono
|
||||
bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white
|
||||
border border-gray-200 dark:border-gray-700 rounded-lg
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
||||
resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<div class="fixed bottom-24 left-1/2 -translate-x-1/2 px-4 py-2 bg-red-500 text-white rounded-lg shadow-lg">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<div
|
||||
class="fixed bottom-24 left-1/2 -translate-x-1/2 px-4 py-2 bg-red-500 text-white rounded-lg shadow-lg"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Versions Modal -->
|
||||
{#if showVersionsModal}
|
||||
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-lg w-full max-h-[80vh] flex flex-col">
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Dokumentversionen</h3>
|
||||
<button
|
||||
onclick={() => showVersionsModal = false}
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
aria-label="Schließen"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-auto p-4">
|
||||
{#if documentVersions.length === 0}
|
||||
<p class="text-center text-gray-500 dark:text-gray-400 py-8">Keine Versionen vorhanden</p>
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
{#each documentVersions as version (version.id)}
|
||||
<button
|
||||
onclick={() => restoreVersion(version)}
|
||||
class="w-full p-3 text-left rounded-lg border border-gray-200 dark:border-gray-700
|
||||
<!-- Versions Modal -->
|
||||
{#if showVersionsModal}
|
||||
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-lg w-full max-h-[80vh] flex flex-col"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Dokumentversionen</h3>
|
||||
<button
|
||||
onclick={() => (showVersionsModal = false)}
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
aria-label="Schließen"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-auto p-4">
|
||||
{#if documentVersions.length === 0}
|
||||
<p class="text-center text-gray-500 dark:text-gray-400 py-8">
|
||||
Keine Versionen vorhanden
|
||||
</p>
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
{#each documentVersions as version (version.id)}
|
||||
<button
|
||||
onclick={() => restoreVersion(version)}
|
||||
class="w-full p-3 text-left rounded-lg border border-gray-200 dark:border-gray-700
|
||||
hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors
|
||||
{version.id === document?.id ? 'ring-2 ring-blue-500' : ''}"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="font-medium text-gray-900 dark:text-white">
|
||||
Version {version.version}
|
||||
{version.id === document?.id ? ' (aktuell)' : ''}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
{new Date(version.created_at).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||
{version.content.substring(0, 100)}...
|
||||
</p>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="font-medium text-gray-900 dark:text-white">
|
||||
Version {version.version}
|
||||
{version.id === document?.id ? ' (aktuell)' : ''}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
{new Date(version.created_at).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||
{version.content.substring(0, 100)}...
|
||||
</p>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,176 +1,180 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { documentService } from '$lib/services/document';
|
||||
import type { DocumentWithConversation } from '@chat/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { documentService } from '$lib/services/document';
|
||||
import type { DocumentWithConversation } from '@chat/types';
|
||||
|
||||
let documents = $state<DocumentWithConversation[]>([]);
|
||||
let isLoading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
let documents = $state<DocumentWithConversation[]>([]);
|
||||
let isLoading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
if (authStore.user) {
|
||||
await loadDocuments();
|
||||
}
|
||||
});
|
||||
onMount(async () => {
|
||||
if (authStore.user) {
|
||||
await loadDocuments();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadDocuments() {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
async function loadDocuments() {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
documents = await documentService.getUserDocuments(authStore.user!.id);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Laden der Dokumente';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
documents = await documentService.getUserDocuments(authStore.user!.id);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Laden der Dokumente';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function extractTitle(content: string): string {
|
||||
// Look for markdown heading level 1 at the start
|
||||
const titleMatch = content.match(/^#\s+(.+)$/m);
|
||||
if (titleMatch && titleMatch[1]) {
|
||||
return titleMatch[1].trim();
|
||||
}
|
||||
function extractTitle(content: string): string {
|
||||
// Look for markdown heading level 1 at the start
|
||||
const titleMatch = content.match(/^#\s+(.+)$/m);
|
||||
if (titleMatch && titleMatch[1]) {
|
||||
return titleMatch[1].trim();
|
||||
}
|
||||
|
||||
// Alternative: Look for heading level 2
|
||||
const subtitleMatch = content.match(/^##\s+(.+)$/m);
|
||||
if (subtitleMatch && subtitleMatch[1]) {
|
||||
return subtitleMatch[1].trim();
|
||||
}
|
||||
// Alternative: Look for heading level 2
|
||||
const subtitleMatch = content.match(/^##\s+(.+)$/m);
|
||||
if (subtitleMatch && subtitleMatch[1]) {
|
||||
return subtitleMatch[1].trim();
|
||||
}
|
||||
|
||||
// If no heading found, take first words
|
||||
const firstLine = content.split('\n')[0].trim();
|
||||
if (firstLine.length > 0) {
|
||||
return firstLine.length > 40 ? `${firstLine.substring(0, 37)}...` : firstLine;
|
||||
}
|
||||
// If no heading found, take first words
|
||||
const firstLine = content.split('\n')[0].trim();
|
||||
if (firstLine.length > 0) {
|
||||
return firstLine.length > 40 ? `${firstLine.substring(0, 37)}...` : firstLine;
|
||||
}
|
||||
|
||||
return 'Dokument ohne Titel';
|
||||
}
|
||||
return 'Dokument ohne Titel';
|
||||
}
|
||||
|
||||
function getPreview(content: string): string {
|
||||
// Remove the first heading if present
|
||||
let preview = content.replace(/^#\s+.+$/m, '').trim();
|
||||
// Take first 200 characters
|
||||
if (preview.length > 200) {
|
||||
preview = preview.substring(0, 200) + '...';
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
function getPreview(content: string): string {
|
||||
// Remove the first heading if present
|
||||
let preview = content.replace(/^#\s+.+$/m, '').trim();
|
||||
// Take first 200 characters
|
||||
if (preview.length > 200) {
|
||||
preview = preview.substring(0, 200) + '...';
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
function navigateToConversation(conversationId: string) {
|
||||
goto(`/chat/${conversationId}`);
|
||||
}
|
||||
function navigateToConversation(conversationId: string) {
|
||||
goto(`/chat/${conversationId}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Dokumente | ManaChat</title>
|
||||
<title>Dokumente | ManaChat</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-[calc(100vh-4rem)] bg-gray-50 dark:bg-gray-900 py-8">
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Dokumente</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Alle Dokumente aus deinen Konversationen im Dokumentmodus.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={loadDocuments}
|
||||
class="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Dokumente</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Alle Dokumente aus deinen Konversationen im Dokumentmodus.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={loadDocuments}
|
||||
class="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200
|
||||
hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
aria-label="Aktualisieren"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
aria-label="Aktualisieren"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
{#if isLoading}
|
||||
<div class="flex items-center justify-center py-16">
|
||||
<div class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"></div>
|
||||
</div>
|
||||
{:else if documents.length === 0}
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-16">
|
||||
<svg
|
||||
class="w-16 h-16 text-gray-400 mx-auto mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-1">Keine Dokumente gefunden</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 max-w-sm mx-auto">
|
||||
Erstelle ein neues Dokument in einer Konversation mit aktiviertem Dokumentmodus.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Documents Grid -->
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each documents as doc (doc.id)}
|
||||
<button
|
||||
onclick={() => navigateToConversation(doc.conversation_id)}
|
||||
class="text-left p-0 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700
|
||||
<!-- Loading State -->
|
||||
{#if isLoading}
|
||||
<div class="flex items-center justify-center py-16">
|
||||
<div
|
||||
class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
{:else if documents.length === 0}
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-16">
|
||||
<svg
|
||||
class="w-16 h-16 text-gray-400 mx-auto mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
||||
Keine Dokumente gefunden
|
||||
</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 max-w-sm mx-auto">
|
||||
Erstelle ein neues Dokument in einer Konversation mit aktiviertem Dokumentmodus.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Documents Grid -->
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each documents as doc (doc.id)}
|
||||
<button
|
||||
onclick={() => navigateToConversation(doc.conversation_id)}
|
||||
class="text-left p-0 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700
|
||||
shadow-sm hover:shadow-md hover:border-blue-300 dark:hover:border-blue-600 transition-all overflow-hidden"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b border-gray-100 dark:border-gray-700">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white line-clamp-2 mb-2">
|
||||
{extractTitle(doc.content)}
|
||||
</h3>
|
||||
<div class="flex items-center justify-between text-xs text-gray-500">
|
||||
<span class="truncate">{doc.conversation_title}</span>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<span>{formatDate(doc.updated_at)}</span>
|
||||
<span class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded font-medium">
|
||||
v{doc.version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b border-gray-100 dark:border-gray-700">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white line-clamp-2 mb-2">
|
||||
{extractTitle(doc.content)}
|
||||
</h3>
|
||||
<div class="flex items-center justify-between text-xs text-gray-500">
|
||||
<span class="truncate">{doc.conversation_title}</span>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<span>{formatDate(doc.updated_at)}</span>
|
||||
<span class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded font-medium">
|
||||
v{doc.version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="p-4 h-32 overflow-hidden">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-5">
|
||||
{getPreview(doc.content)}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Preview -->
|
||||
<div class="p-4 h-32 overflow-hidden">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-5">
|
||||
{getPreview(doc.content)}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<div class="mt-4 p-4 bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-400 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<div class="mt-4 p-4 bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-400 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,132 +1,149 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
|
||||
function handleSignOut() {
|
||||
authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
function handleSignOut() {
|
||||
authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
theme.toggleMode();
|
||||
}
|
||||
function toggleTheme() {
|
||||
theme.toggleMode();
|
||||
}
|
||||
|
||||
function formatDate(dateString: string | undefined): string {
|
||||
if (!dateString) return '-';
|
||||
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
function formatDate(dateString: string | undefined): string {
|
||||
if (!dateString) return '-';
|
||||
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Profil | ManaChat</title>
|
||||
<title>Profil | ManaChat</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-[calc(100vh-4rem)] bg-gray-50 dark:bg-gray-900 py-8">
|
||||
<div class="max-w-2xl mx-auto px-4">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Profil</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Verwalte dein Konto und deine Einstellungen.
|
||||
</p>
|
||||
</div>
|
||||
<div class="max-w-2xl mx-auto px-4">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Profil</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Verwalte dein Konto und deine Einstellungen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Profile Card -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden mb-6">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{authStore.user?.email || 'Benutzer'}
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500">
|
||||
Mitglied seit {formatDate(authStore.user?.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Profile Card -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden mb-6"
|
||||
>
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div
|
||||
class="w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8 text-blue-600 dark:text-blue-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{authStore.user?.email || 'Benutzer'}
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500">
|
||||
Mitglied seit {formatDate(authStore.user?.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between py-3 border-b border-gray-100 dark:border-gray-700">
|
||||
<div>
|
||||
<p class="font-medium text-gray-900 dark:text-white">E-Mail</p>
|
||||
<p class="text-sm text-gray-500">{authStore.user?.email || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-3 border-b border-gray-100 dark:border-gray-700">
|
||||
<div>
|
||||
<p class="font-medium text-gray-900 dark:text-white">Benutzer-ID</p>
|
||||
<p class="text-sm text-gray-500 font-mono">{authStore.user?.id || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
class="flex items-center justify-between py-3 border-b border-gray-100 dark:border-gray-700"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-gray-900 dark:text-white">E-Mail</p>
|
||||
<p class="text-sm text-gray-500">{authStore.user?.email || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between py-3 border-b border-gray-100 dark:border-gray-700"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-gray-900 dark:text-white">Benutzer-ID</p>
|
||||
<p class="text-sm text-gray-500 font-mono">{authStore.user?.id || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Card -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden mb-6">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Einstellungen</h3>
|
||||
<!-- Settings Card -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden mb-6"
|
||||
>
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Einstellungen</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Theme Toggle -->
|
||||
<div class="flex items-center justify-between py-3">
|
||||
<div>
|
||||
<p class="font-medium text-gray-900 dark:text-white">Dunkler Modus</p>
|
||||
<p class="text-sm text-gray-500">Aktiviere den dunklen Modus für die App</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={toggleTheme}
|
||||
class="relative w-12 h-6 rounded-full transition-colors
|
||||
<div class="space-y-4">
|
||||
<!-- Theme Toggle -->
|
||||
<div class="flex items-center justify-between py-3">
|
||||
<div>
|
||||
<p class="font-medium text-gray-900 dark:text-white">Dunkler Modus</p>
|
||||
<p class="text-sm text-gray-500">Aktiviere den dunklen Modus für die App</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={toggleTheme}
|
||||
class="relative w-12 h-6 rounded-full transition-colors
|
||||
{theme.mode === 'dark' ? 'bg-blue-600' : 'bg-gray-300'}"
|
||||
role="switch"
|
||||
aria-checked={theme.mode === 'dark'}
|
||||
aria-label="Dunkler Modus umschalten"
|
||||
>
|
||||
<span
|
||||
class="absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow transition-transform
|
||||
role="switch"
|
||||
aria-checked={theme.mode === 'dark'}
|
||||
aria-label="Dunkler Modus umschalten"
|
||||
>
|
||||
<span
|
||||
class="absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow transition-transform
|
||||
{theme.mode === 'dark' ? 'translate-x-6' : 'translate-x-0'}"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sign Out -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden">
|
||||
<div class="p-6">
|
||||
<button
|
||||
onclick={handleSignOut}
|
||||
class="w-full flex items-center justify-center gap-2 px-4 py-3 bg-red-50 dark:bg-red-900/20
|
||||
<!-- Sign Out -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden"
|
||||
>
|
||||
<div class="p-6">
|
||||
<button
|
||||
onclick={handleSignOut}
|
||||
class="w-full flex items-center justify-center gap-2 px-4 py-3 bg-red-50 dark:bg-red-900/20
|
||||
text-red-600 dark:text-red-400 rounded-lg font-medium
|
||||
hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,175 +1,193 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { spacesStore } from '$lib/stores/spaces.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import SpaceCard from '$lib/components/spaces/SpaceCard.svelte';
|
||||
import SpaceForm from '$lib/components/spaces/SpaceForm.svelte';
|
||||
import type { Space } from '@chat/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { spacesStore } from '$lib/stores/spaces.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import SpaceCard from '$lib/components/spaces/SpaceCard.svelte';
|
||||
import SpaceForm from '$lib/components/spaces/SpaceForm.svelte';
|
||||
import type { Space } from '@chat/types';
|
||||
|
||||
let showForm = $state(false);
|
||||
let editingSpace = $state<Space | undefined>(undefined);
|
||||
let showForm = $state(false);
|
||||
let editingSpace = $state<Space | undefined>(undefined);
|
||||
|
||||
onMount(async () => {
|
||||
if (authStore.user) {
|
||||
await spacesStore.loadSpaces(authStore.user.id);
|
||||
}
|
||||
});
|
||||
onMount(async () => {
|
||||
if (authStore.user) {
|
||||
await spacesStore.loadSpaces(authStore.user.id);
|
||||
}
|
||||
});
|
||||
|
||||
function handleCreateNew() {
|
||||
editingSpace = undefined;
|
||||
showForm = true;
|
||||
}
|
||||
function handleCreateNew() {
|
||||
editingSpace = undefined;
|
||||
showForm = true;
|
||||
}
|
||||
|
||||
function handleSelect(id: string) {
|
||||
// Navigate to space - load conversations filtered by this space
|
||||
goto(`/spaces/${id}`);
|
||||
}
|
||||
function handleSelect(id: string) {
|
||||
// Navigate to space - load conversations filtered by this space
|
||||
goto(`/spaces/${id}`);
|
||||
}
|
||||
|
||||
function handleEdit(id: string) {
|
||||
const space = spacesStore.spaces.find((s) => s.id === id);
|
||||
if (space) {
|
||||
editingSpace = space;
|
||||
showForm = true;
|
||||
}
|
||||
}
|
||||
function handleEdit(id: string) {
|
||||
const space = spacesStore.spaces.find((s) => s.id === id);
|
||||
if (space) {
|
||||
editingSpace = space;
|
||||
showForm = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (confirm('Möchtest du diesen Space wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) {
|
||||
await spacesStore.deleteSpace(id);
|
||||
}
|
||||
}
|
||||
async function handleDelete(id: string) {
|
||||
if (
|
||||
confirm(
|
||||
'Möchtest du diesen Space wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.'
|
||||
)
|
||||
) {
|
||||
await spacesStore.deleteSpace(id);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLeave(id: string) {
|
||||
if (!authStore.user) return;
|
||||
async function handleLeave(id: string) {
|
||||
if (!authStore.user) return;
|
||||
|
||||
if (confirm('Möchtest du diesen Space wirklich verlassen?')) {
|
||||
await spacesStore.leaveSpace(id, authStore.user.id);
|
||||
}
|
||||
}
|
||||
if (confirm('Möchtest du diesen Space wirklich verlassen?')) {
|
||||
await spacesStore.leaveSpace(id, authStore.user.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(data: { name: string; description?: string }) {
|
||||
if (!authStore.user) return;
|
||||
async function handleSubmit(data: { name: string; description?: string }) {
|
||||
if (!authStore.user) return;
|
||||
|
||||
if (editingSpace) {
|
||||
// Update existing space
|
||||
await spacesStore.updateSpace(editingSpace.id, data);
|
||||
} else {
|
||||
// Create new space
|
||||
await spacesStore.createSpace({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
owner_id: authStore.user.id,
|
||||
});
|
||||
}
|
||||
if (editingSpace) {
|
||||
// Update existing space
|
||||
await spacesStore.updateSpace(editingSpace.id, data);
|
||||
} else {
|
||||
// Create new space
|
||||
await spacesStore.createSpace({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
owner_id: authStore.user.id,
|
||||
});
|
||||
}
|
||||
|
||||
showForm = false;
|
||||
editingSpace = undefined;
|
||||
}
|
||||
showForm = false;
|
||||
editingSpace = undefined;
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
showForm = false;
|
||||
editingSpace = undefined;
|
||||
}
|
||||
function handleCancel() {
|
||||
showForm = false;
|
||||
editingSpace = undefined;
|
||||
}
|
||||
|
||||
function isOwner(space: Space): boolean {
|
||||
return space.owner_id === authStore.user?.id;
|
||||
}
|
||||
function isOwner(space: Space): boolean {
|
||||
return space.owner_id === authStore.user?.id;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Spaces | ManaChat</title>
|
||||
<title>Spaces | ManaChat</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-[calc(100vh-4rem)] bg-gray-50 dark:bg-gray-900 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Spaces</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Organisiere deine Konversationen in kollaborativen Arbeitsbereichen.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={handleCreateNew}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
|
||||
<div class="max-w-4xl mx-auto px-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Spaces</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Organisiere deine Konversationen in kollaborativen Arbeitsbereichen.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={handleCreateNew}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
|
||||
hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Neuen Space erstellen
|
||||
</button>
|
||||
</div>
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Neuen Space erstellen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
{#if spacesStore.isLoading}
|
||||
<div class="flex items-center justify-center py-16">
|
||||
<div class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"></div>
|
||||
</div>
|
||||
{:else if spacesStore.spaces.length === 0}
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-16">
|
||||
<svg
|
||||
class="w-16 h-16 text-gray-400 mx-auto mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-1">Keine Spaces gefunden</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">
|
||||
Erstelle einen neuen Space oder frage nach einer Einladung
|
||||
</p>
|
||||
<button
|
||||
onclick={handleCreateNew}
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
|
||||
<!-- Loading State -->
|
||||
{#if spacesStore.isLoading}
|
||||
<div class="flex items-center justify-center py-16">
|
||||
<div
|
||||
class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
{:else if spacesStore.spaces.length === 0}
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-16">
|
||||
<svg
|
||||
class="w-16 h-16 text-gray-400 mx-auto mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
||||
Keine Spaces gefunden
|
||||
</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">
|
||||
Erstelle einen neuen Space oder frage nach einer Einladung
|
||||
</p>
|
||||
<button
|
||||
onclick={handleCreateNew}
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
|
||||
hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Ersten Space erstellen
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Spaces Grid -->
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
{#each spacesStore.spaces as space (space.id)}
|
||||
<SpaceCard
|
||||
{space}
|
||||
isOwner={isOwner(space)}
|
||||
onSelect={handleSelect}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onLeave={handleLeave}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Ersten Space erstellen
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Spaces Grid -->
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
{#each spacesStore.spaces as space (space.id)}
|
||||
<SpaceCard
|
||||
{space}
|
||||
isOwner={isOwner(space)}
|
||||
onSelect={handleSelect}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onLeave={handleLeave}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if spacesStore.error}
|
||||
<div class="mt-4 p-4 bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-400 rounded-lg">
|
||||
{spacesStore.error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Error Message -->
|
||||
{#if spacesStore.error}
|
||||
<div class="mt-4 p-4 bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-400 rounded-lg">
|
||||
{spacesStore.error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Modal -->
|
||||
{#if showForm}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||
<div class="w-full max-w-lg max-h-[90vh] overflow-y-auto rounded-xl shadow-xl">
|
||||
<SpaceForm space={editingSpace} onSubmit={handleSubmit} onCancel={handleCancel} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||
<div class="w-full max-w-lg max-h-[90vh] overflow-y-auto rounded-xl shadow-xl">
|
||||
<SpaceForm space={editingSpace} onSubmit={handleSubmit} onCancel={handleCancel} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,188 +1,199 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { spaceService } from '$lib/services/space';
|
||||
import { conversationService } from '$lib/services/conversation';
|
||||
import { chatService } from '$lib/services/chat';
|
||||
import type { Space, Conversation, AIModel } from '@chat/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { spaceService } from '$lib/services/space';
|
||||
import { conversationService } from '$lib/services/conversation';
|
||||
import { chatService } from '$lib/services/chat';
|
||||
import type { Space, Conversation, AIModel } from '@chat/types';
|
||||
|
||||
const spaceId = $derived($page.params.id ?? '');
|
||||
const spaceId = $derived($page.params.id ?? '');
|
||||
|
||||
let space = $state<Space | null>(null);
|
||||
let conversations = $state<Conversation[]>([]);
|
||||
let models = $state<AIModel[]>([]);
|
||||
let selectedModelId = $state('');
|
||||
let isLoading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
let space = $state<Space | null>(null);
|
||||
let conversations = $state<Conversation[]>([]);
|
||||
let models = $state<AIModel[]>([]);
|
||||
let selectedModelId = $state('');
|
||||
let isLoading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
await loadData();
|
||||
});
|
||||
onMount(async () => {
|
||||
await loadData();
|
||||
});
|
||||
|
||||
async function loadData() {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
async function loadData() {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Load space details
|
||||
space = await spaceService.getSpace(spaceId);
|
||||
if (!space) {
|
||||
error = 'Space nicht gefunden';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Load space details
|
||||
space = await spaceService.getSpace(spaceId);
|
||||
if (!space) {
|
||||
error = 'Space nicht gefunden';
|
||||
return;
|
||||
}
|
||||
|
||||
// Load conversations in this space
|
||||
if (authStore.user) {
|
||||
conversations = await conversationService.getConversations(authStore.user.id, spaceId);
|
||||
}
|
||||
// Load conversations in this space
|
||||
if (authStore.user) {
|
||||
conversations = await conversationService.getConversations(authStore.user.id, spaceId);
|
||||
}
|
||||
|
||||
// Load models
|
||||
models = await chatService.getModels();
|
||||
if (models.length > 0) {
|
||||
selectedModelId = models[0].id;
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Laden';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
// Load models
|
||||
models = await chatService.getModels();
|
||||
if (models.length > 0) {
|
||||
selectedModelId = models[0].id;
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Laden';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNewChat() {
|
||||
if (!authStore.user || !selectedModelId) return;
|
||||
async function handleNewChat() {
|
||||
if (!authStore.user || !selectedModelId) return;
|
||||
|
||||
const conversationId = await conversationService.createConversation(
|
||||
authStore.user.id,
|
||||
selectedModelId,
|
||||
'free',
|
||||
undefined,
|
||||
false,
|
||||
spaceId
|
||||
);
|
||||
const conversationId = await conversationService.createConversation(
|
||||
authStore.user.id,
|
||||
selectedModelId,
|
||||
'free',
|
||||
undefined,
|
||||
false,
|
||||
spaceId
|
||||
);
|
||||
|
||||
if (conversationId) {
|
||||
goto(`/chat/${conversationId}`);
|
||||
}
|
||||
}
|
||||
if (conversationId) {
|
||||
goto(`/chat/${conversationId}`);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{space?.name || 'Space'} | ManaChat</title>
|
||||
<title>{space?.name || 'Space'} | ManaChat</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="flex items-center justify-center h-[calc(100vh-4rem)]">
|
||||
<div class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"></div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center h-[calc(100vh-4rem)]">
|
||||
<div
|
||||
class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="flex flex-col items-center justify-center h-[calc(100vh-4rem)] text-center p-4">
|
||||
<p class="text-red-500 mb-4">{error}</p>
|
||||
<a href="/spaces" class="text-blue-600 hover:underline">Zurück zu Spaces</a>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center h-[calc(100vh-4rem)] text-center p-4">
|
||||
<p class="text-red-500 mb-4">{error}</p>
|
||||
<a href="/spaces" class="text-blue-600 hover:underline">Zurück zu Spaces</a>
|
||||
</div>
|
||||
{:else if space}
|
||||
<div class="min-h-[calc(100vh-4rem)] bg-gray-50 dark:bg-gray-900 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<a
|
||||
href="/spaces"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
aria-label="Zurück zu Spaces"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{space.name}</h1>
|
||||
</div>
|
||||
{#if space.description}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{space.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="min-h-[calc(100vh-4rem)] bg-gray-50 dark:bg-gray-900 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<a
|
||||
href="/spaces"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
aria-label="Zurück zu Spaces"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{space.name}</h1>
|
||||
</div>
|
||||
{#if space.description}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{space.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- New Chat Section -->
|
||||
<div class="mb-8 p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">Neuen Chat starten</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
<select
|
||||
bind:value={selectedModelId}
|
||||
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
<!-- New Chat Section -->
|
||||
<div
|
||||
class="mb-8 p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">Neuen Chat starten</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
<select
|
||||
bind:value={selectedModelId}
|
||||
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
{#each models as model}
|
||||
<option value={model.id}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button
|
||||
onclick={handleNewChat}
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
|
||||
>
|
||||
{#each models as model}
|
||||
<option value={model.id}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button
|
||||
onclick={handleNewChat}
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
|
||||
hover:bg-blue-700 transition-colors whitespace-nowrap"
|
||||
>
|
||||
Chat starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
>
|
||||
Chat starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conversations List -->
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Konversationen in diesem Space
|
||||
</h2>
|
||||
<!-- Conversations List -->
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Konversationen in diesem Space
|
||||
</h2>
|
||||
|
||||
{#if conversations.length === 0}
|
||||
<div class="text-center py-12 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700">
|
||||
<svg
|
||||
class="w-12 h-12 text-gray-400 mx-auto mb-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
Noch keine Konversationen in diesem Space.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each conversations as conv (conv.id)}
|
||||
<a
|
||||
href="/chat/{conv.id}"
|
||||
class="block p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700
|
||||
{#if conversations.length === 0}
|
||||
<div
|
||||
class="text-center py-12 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<svg
|
||||
class="w-12 h-12 text-gray-400 mx-auto mb-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
Noch keine Konversationen in diesem Space.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each conversations as conv (conv.id)}
|
||||
<a
|
||||
href="/chat/{conv.id}"
|
||||
class="block p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700
|
||||
hover:border-blue-300 dark:hover:border-blue-600 transition-colors"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-medium text-gray-900 dark:text-white">
|
||||
{conv.title || 'Neue Konversation'}
|
||||
</h3>
|
||||
<span class="text-xs text-gray-500">{formatDate(conv.updated_at)}</span>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-medium text-gray-900 dark:text-white">
|
||||
{conv.title || 'Neue Konversation'}
|
||||
</h3>
|
||||
<span class="text-xs text-gray-500">{formatDate(conv.updated_at)}</span>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,197 +1,212 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { templatesStore } from '$lib/stores/templates.svelte';
|
||||
import { conversationService } from '$lib/services/conversation';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import TemplateCard from '$lib/components/templates/TemplateCard.svelte';
|
||||
import TemplateForm from '$lib/components/templates/TemplateForm.svelte';
|
||||
import type { Template } from '@chat/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { templatesStore } from '$lib/stores/templates.svelte';
|
||||
import { conversationService } from '$lib/services/conversation';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import TemplateCard from '$lib/components/templates/TemplateCard.svelte';
|
||||
import TemplateForm from '$lib/components/templates/TemplateForm.svelte';
|
||||
import type { Template } from '@chat/types';
|
||||
|
||||
let showForm = $state(false);
|
||||
let editingTemplate = $state<Template | undefined>(undefined);
|
||||
let showForm = $state(false);
|
||||
let editingTemplate = $state<Template | undefined>(undefined);
|
||||
|
||||
onMount(async () => {
|
||||
if (authStore.user) {
|
||||
await templatesStore.loadTemplates(authStore.user.id);
|
||||
}
|
||||
});
|
||||
onMount(async () => {
|
||||
if (authStore.user) {
|
||||
await templatesStore.loadTemplates(authStore.user.id);
|
||||
}
|
||||
});
|
||||
|
||||
function handleCreateNew() {
|
||||
editingTemplate = undefined;
|
||||
showForm = true;
|
||||
}
|
||||
function handleCreateNew() {
|
||||
editingTemplate = undefined;
|
||||
showForm = true;
|
||||
}
|
||||
|
||||
function handleEdit(id: string) {
|
||||
const template = templatesStore.templates.find((t) => t.id === id);
|
||||
if (template) {
|
||||
editingTemplate = template;
|
||||
showForm = true;
|
||||
}
|
||||
}
|
||||
function handleEdit(id: string) {
|
||||
const template = templatesStore.templates.find((t) => t.id === id);
|
||||
if (template) {
|
||||
editingTemplate = template;
|
||||
showForm = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (confirm('Möchtest du diese Vorlage wirklich löschen?')) {
|
||||
await templatesStore.deleteTemplate(id);
|
||||
}
|
||||
}
|
||||
async function handleDelete(id: string) {
|
||||
if (confirm('Möchtest du diese Vorlage wirklich löschen?')) {
|
||||
await templatesStore.deleteTemplate(id);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSetDefault(id: string) {
|
||||
if (authStore.user) {
|
||||
await templatesStore.setDefaultTemplate(id, authStore.user.id);
|
||||
}
|
||||
}
|
||||
async function handleSetDefault(id: string) {
|
||||
if (authStore.user) {
|
||||
await templatesStore.setDefaultTemplate(id, authStore.user.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUse(id: string) {
|
||||
const template = templatesStore.templates.find((t) => t.id === id);
|
||||
if (!template || !authStore.user) return;
|
||||
async function handleUse(id: string) {
|
||||
const template = templatesStore.templates.find((t) => t.id === id);
|
||||
if (!template || !authStore.user) return;
|
||||
|
||||
// Create a new conversation with this template
|
||||
const conversationId = await conversationService.createConversation(
|
||||
authStore.user.id,
|
||||
template.model_id || '550e8400-e29b-41d4-a716-446655440004', // Default to GPT-4o-Mini
|
||||
'template',
|
||||
template.id,
|
||||
template.document_mode
|
||||
);
|
||||
// Create a new conversation with this template
|
||||
const conversationId = await conversationService.createConversation(
|
||||
authStore.user.id,
|
||||
template.model_id || '550e8400-e29b-41d4-a716-446655440004', // Default to GPT-4o-Mini
|
||||
'template',
|
||||
template.id,
|
||||
template.document_mode
|
||||
);
|
||||
|
||||
if (conversationId) {
|
||||
await conversationsStore.loadConversations(authStore.user.id);
|
||||
goto(`/chat/${conversationId}`);
|
||||
}
|
||||
}
|
||||
if (conversationId) {
|
||||
await conversationsStore.loadConversations(authStore.user.id);
|
||||
goto(`/chat/${conversationId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(data: Partial<Template>) {
|
||||
if (!authStore.user) return;
|
||||
async function handleSubmit(data: Partial<Template>) {
|
||||
if (!authStore.user) return;
|
||||
|
||||
if (data.id) {
|
||||
// Update existing template
|
||||
await templatesStore.updateTemplate(data.id, {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
system_prompt: data.system_prompt,
|
||||
initial_question: data.initial_question,
|
||||
color: data.color,
|
||||
model_id: data.model_id,
|
||||
document_mode: data.document_mode,
|
||||
});
|
||||
} else {
|
||||
// Create new template
|
||||
await templatesStore.createTemplate({
|
||||
user_id: authStore.user.id,
|
||||
name: data.name!,
|
||||
description: data.description ?? null,
|
||||
system_prompt: data.system_prompt!,
|
||||
initial_question: data.initial_question ?? null,
|
||||
color: data.color!,
|
||||
model_id: data.model_id ?? null,
|
||||
is_default: false,
|
||||
document_mode: data.document_mode ?? false,
|
||||
});
|
||||
}
|
||||
if (data.id) {
|
||||
// Update existing template
|
||||
await templatesStore.updateTemplate(data.id, {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
system_prompt: data.system_prompt,
|
||||
initial_question: data.initial_question,
|
||||
color: data.color,
|
||||
model_id: data.model_id,
|
||||
document_mode: data.document_mode,
|
||||
});
|
||||
} else {
|
||||
// Create new template
|
||||
await templatesStore.createTemplate({
|
||||
user_id: authStore.user.id,
|
||||
name: data.name!,
|
||||
description: data.description ?? null,
|
||||
system_prompt: data.system_prompt!,
|
||||
initial_question: data.initial_question ?? null,
|
||||
color: data.color!,
|
||||
model_id: data.model_id ?? null,
|
||||
is_default: false,
|
||||
document_mode: data.document_mode ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
showForm = false;
|
||||
editingTemplate = undefined;
|
||||
}
|
||||
showForm = false;
|
||||
editingTemplate = undefined;
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
showForm = false;
|
||||
editingTemplate = undefined;
|
||||
}
|
||||
function handleCancel() {
|
||||
showForm = false;
|
||||
editingTemplate = undefined;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Vorlagen | ManaChat</title>
|
||||
<title>Vorlagen | ManaChat</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-[calc(100vh-4rem)] bg-gray-50 dark:bg-gray-900 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Vorlagen</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Erstelle Vorlagen mit benutzerdefinierten System-Prompts für verschiedene KI-Verhaltensweisen.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={handleCreateNew}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
|
||||
<div class="max-w-4xl mx-auto px-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Vorlagen</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Erstelle Vorlagen mit benutzerdefinierten System-Prompts für verschiedene
|
||||
KI-Verhaltensweisen.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={handleCreateNew}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
|
||||
hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Neue Vorlage
|
||||
</button>
|
||||
</div>
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Neue Vorlage
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
{#if templatesStore.isLoading}
|
||||
<div class="flex items-center justify-center py-16">
|
||||
<div class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"></div>
|
||||
</div>
|
||||
{:else if templatesStore.templates.length === 0}
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-16">
|
||||
<svg
|
||||
class="w-16 h-16 text-gray-400 mx-auto mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-1">Keine Vorlagen vorhanden</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">
|
||||
Erstelle deine erste Vorlage, um loszulegen
|
||||
</p>
|
||||
<button
|
||||
onclick={handleCreateNew}
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
|
||||
<!-- Loading State -->
|
||||
{#if templatesStore.isLoading}
|
||||
<div class="flex items-center justify-center py-16">
|
||||
<div
|
||||
class="animate-spin w-8 h-8 border-4 border-blue-500 border-r-transparent rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
{:else if templatesStore.templates.length === 0}
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-16">
|
||||
<svg
|
||||
class="w-16 h-16 text-gray-400 mx-auto mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
||||
Keine Vorlagen vorhanden
|
||||
</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">
|
||||
Erstelle deine erste Vorlage, um loszulegen
|
||||
</p>
|
||||
<button
|
||||
onclick={handleCreateNew}
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
|
||||
hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Erste Vorlage erstellen
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Templates Grid -->
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
{#each templatesStore.templates as template (template.id)}
|
||||
<TemplateCard
|
||||
{template}
|
||||
onUse={handleUse}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onSetDefault={handleSetDefault}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Erste Vorlage erstellen
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Templates Grid -->
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
{#each templatesStore.templates as template (template.id)}
|
||||
<TemplateCard
|
||||
{template}
|
||||
onUse={handleUse}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onSetDefault={handleSetDefault}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if templatesStore.error}
|
||||
<div class="mt-4 p-4 bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-400 rounded-lg">
|
||||
{templatesStore.error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Error Message -->
|
||||
{#if templatesStore.error}
|
||||
<div class="mt-4 p-4 bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-400 rounded-lg">
|
||||
{templatesStore.error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Modal -->
|
||||
{#if showForm}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||
<div class="w-full max-w-2xl max-h-[90vh] overflow-y-auto rounded-xl shadow-xl">
|
||||
<TemplateForm template={editingTemplate} onSubmit={handleSubmit} onCancel={handleCancel} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||
<div class="w-full max-w-2xl max-h-[90vh] overflow-y-auto rounded-xl shadow-xl">
|
||||
<TemplateForm template={editingTemplate} onSubmit={handleSubmit} onCancel={handleCancel} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
|
||||
if (authStore.isAuthenticated) {
|
||||
goto('/chat', { replaceState: true });
|
||||
} else {
|
||||
goto('/login', { replaceState: true });
|
||||
}
|
||||
});
|
||||
if (authStore.isAuthenticated) {
|
||||
goto('/chat', { replaceState: true });
|
||||
} else {
|
||||
goto('/login', { replaceState: true });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>ManaChat - AI Chat Assistant</title>
|
||||
<meta name="description" content="Chat with AI models - GPT-4o, GPT-4o-Mini, and more" />
|
||||
<title>ManaChat - AI Chat Assistant</title>
|
||||
<meta name="description" content="Chat with AI models - GPT-4o, GPT-4o-Mini, and more" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<div class="text-center">
|
||||
<div class="animate-spin w-10 h-10 border-4 border-blue-500 border-r-transparent rounded-full mx-auto"></div>
|
||||
<p class="mt-4 text-gray-600 dark:text-gray-400">Wird geladen...</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="animate-spin w-10 h-10 border-4 border-blue-500 border-r-transparent rounded-full mx-auto"
|
||||
></div>
|
||||
<p class="mt-4 text-gray-600 dark:text-gray-400">Wird geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ const config = {
|
|||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
}
|
||||
adapter: adapter(),
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ export default defineConfig({
|
|||
'@manacore/shared-auth-ui',
|
||||
'@manacore/shared-branding',
|
||||
'@manacore/shared-ui',
|
||||
'@manacore/shared-theme-ui'
|
||||
]
|
||||
'@manacore/shared-theme-ui',
|
||||
],
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
|
|
@ -19,7 +19,7 @@ export default defineConfig({
|
|||
'@manacore/shared-auth-ui',
|
||||
'@manacore/shared-branding',
|
||||
'@manacore/shared-ui',
|
||||
'@manacore/shared-theme-ui'
|
||||
]
|
||||
}
|
||||
'@manacore/shared-theme-ui',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue