mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
fix(chat): align frontend types with backend camelCase and redesign message bubbles
- Update all frontend types and components to use camelCase properties matching backend API - Redesign MessageBubble with gradient avatars, shadows, and modern speech bubble styling - Update TypingIndicator to match new bubble design with Robot avatar - Fix marked.js undefined input error by ensuring proper property access 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a3247086eb
commit
0467ac3891
24 changed files with 532 additions and 370 deletions
|
|
@ -1,17 +1,41 @@
|
|||
<script lang="ts">
|
||||
import { PaperPlaneTilt } from '@manacore/shared-icons';
|
||||
import { PaperPlaneTilt, FileText, CaretDown } from '@manacore/shared-icons';
|
||||
import type { AIModel } from '@chat/types';
|
||||
|
||||
interface Props {
|
||||
onSend: (message: string) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
models?: AIModel[];
|
||||
selectedModelId?: string;
|
||||
onModelSelect?: (modelId: string) => void;
|
||||
documentMode?: boolean;
|
||||
onDocumentModeToggle?: () => void;
|
||||
}
|
||||
|
||||
let { onSend, disabled = false, placeholder = 'Nachricht eingeben...' }: Props = $props();
|
||||
let {
|
||||
onSend,
|
||||
disabled = false,
|
||||
placeholder = 'Nachricht eingeben...',
|
||||
models = [],
|
||||
selectedModelId = '',
|
||||
onModelSelect,
|
||||
documentMode = false,
|
||||
onDocumentModeToggle,
|
||||
}: Props = $props();
|
||||
|
||||
let inputValue = $state('');
|
||||
let textareaEl: HTMLTextAreaElement | undefined = $state();
|
||||
|
||||
// Focus input on mount
|
||||
let hasFocused = false;
|
||||
$effect(() => {
|
||||
if (textareaEl && !disabled && !hasFocused) {
|
||||
textareaEl.focus();
|
||||
hasFocused = true;
|
||||
}
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
const trimmed = inputValue.trim();
|
||||
if (trimmed && !disabled) {
|
||||
|
|
@ -42,35 +66,82 @@
|
|||
|
||||
<div class="relative">
|
||||
<div
|
||||
class="flex items-end gap-3 rounded-2xl border border-border bg-white/70 dark:bg-black/50 backdrop-blur-xl p-2 shadow-lg"
|
||||
class="flex flex-col gap-2 rounded-2xl bg-white/80 dark:bg-white/10 backdrop-blur-xl border border-black/10 dark:border-white/20 p-2 shadow-lg"
|
||||
>
|
||||
<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-0 bg-transparent
|
||||
px-4 py-3 text-sm text-foreground
|
||||
focus:outline-none focus:ring-0
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
placeholder:text-muted-foreground"
|
||||
></textarea>
|
||||
<!-- Input Row -->
|
||||
<div class="flex items-end gap-3">
|
||||
<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-0 bg-transparent
|
||||
px-4 py-3 text-sm text-foreground
|
||||
focus:outline-none focus:ring-0
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
placeholder:text-muted-foreground"
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
onclick={handleSubmit}
|
||||
disabled={disabled || !inputValue.trim()}
|
||||
aria-label="Nachricht senden"
|
||||
class="flex-shrink-0 p-3 rounded-xl bg-white/90 dark:bg-white/20 backdrop-blur-sm
|
||||
border border-black/10 dark:border-white/20 text-primary
|
||||
hover:bg-white dark:hover:bg-white/30 hover:shadow-lg
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
transition-all duration-200 shadow-md"
|
||||
>
|
||||
<PaperPlaneTilt size={20} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onclick={handleSubmit}
|
||||
disabled={disabled || !inputValue.trim()}
|
||||
aria-label="Nachricht senden"
|
||||
class="flex-shrink-0 p-3 rounded-xl bg-primary text-primary-foreground
|
||||
hover:bg-primary/90 active:bg-primary/80
|
||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-primary
|
||||
transition-all duration-200 shadow-md hover:shadow-lg"
|
||||
>
|
||||
<PaperPlaneTilt size={20} weight="bold" />
|
||||
</button>
|
||||
|
||||
<!-- Options Row -->
|
||||
{#if models.length > 0 || onDocumentModeToggle}
|
||||
<div class="flex items-center gap-2 px-2">
|
||||
<!-- Model Selector -->
|
||||
{#if models.length > 0 && onModelSelect}
|
||||
<div class="relative">
|
||||
<select
|
||||
value={selectedModelId}
|
||||
onchange={(e) => onModelSelect?.((e.target as HTMLSelectElement).value)}
|
||||
{disabled}
|
||||
class="appearance-none bg-white/50 dark:bg-white/10 text-foreground text-xs font-medium rounded-lg pl-3 pr-7 py-1.5 border border-black/5 dark:border-white/10 focus:outline-none focus:ring-1 focus:ring-primary/50 disabled:opacity-50 cursor-pointer transition-colors hover:bg-white/80 dark:hover:bg-white/20"
|
||||
>
|
||||
{#each models as model (model.id)}
|
||||
<option value={model.id}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<div class="absolute inset-y-0 right-1.5 flex items-center pointer-events-none">
|
||||
<CaretDown size={12} weight="bold" class="text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Spacer -->
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<!-- Document Mode Toggle -->
|
||||
{#if onDocumentModeToggle}
|
||||
<button
|
||||
onclick={onDocumentModeToggle}
|
||||
{disabled}
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-all duration-200 disabled:opacity-50
|
||||
{documentMode
|
||||
? 'bg-primary/20 text-primary border border-primary/30'
|
||||
: 'bg-white/50 dark:bg-white/10 text-muted-foreground border border-black/5 dark:border-white/10 hover:bg-white/80 dark:hover:bg-white/20 hover:text-foreground'}"
|
||||
title="Dokumentmodus aktivieren"
|
||||
>
|
||||
<FileText size={14} weight={documentMode ? 'fill' : 'bold'} />
|
||||
<span>Dokument</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground text-center mt-2 opacity-70">
|
||||
Enter zum Senden, Shift+Enter für neue Zeile
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { isSidebarMode, isNavCollapsed } from '$lib/stores/navigation';
|
||||
import { MagnifyingGlass, X, Plus, ChatCircle } from '@manacore/shared-icons';
|
||||
import { MagnifyingGlass, X, Plus, ChatCircle, Archive, Trash } from '@manacore/shared-icons';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -94,6 +95,28 @@
|
|||
function isActive(convId: string): boolean {
|
||||
return $page.params.id === convId;
|
||||
}
|
||||
|
||||
// Archive conversation
|
||||
async function handleArchive(e: MouseEvent, convId: string) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const success = await conversationsStore.archiveConversation(convId);
|
||||
if (success && isActive(convId)) {
|
||||
goto('/chat');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete conversation
|
||||
async function handleDelete(e: MouseEvent, convId: string) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (confirm('Möchtest du diese Konversation wirklich löschen?')) {
|
||||
const success = await conversationsStore.deleteConversation(convId);
|
||||
if (success && isActive(convId)) {
|
||||
goto('/chat');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
|
@ -183,7 +206,7 @@
|
|||
{#each filteredConversations as conv (conv.id)}
|
||||
<a
|
||||
href="/chat/{conv.id}"
|
||||
class="block w-full rounded-xl bg-white/60 dark:bg-white/5 backdrop-blur-sm border border-black/10 dark:border-white/20 p-4 text-left transition-all mb-3 hover:shadow-md hover:bg-white/80 dark:hover:bg-white/10
|
||||
class="group block w-full rounded-xl bg-white/60 dark:bg-white/5 backdrop-blur-sm border border-black/10 dark:border-white/20 p-4 text-left transition-all mb-3 hover:shadow-md hover:bg-white/80 dark:hover:bg-white/10
|
||||
{isActive(conv.id)
|
||||
? 'bg-white/90 dark:bg-white/15 shadow-md border-primary/30'
|
||||
: ''}"
|
||||
|
|
@ -210,15 +233,34 @@
|
|||
<!-- Footer -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{formatDate(conv.updated_at || conv.created_at)}
|
||||
{formatDate(conv.updatedAt || conv.createdAt)}
|
||||
</span>
|
||||
{#if conv.document_mode}
|
||||
<span
|
||||
class="text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full"
|
||||
>
|
||||
Dokument
|
||||
</span>
|
||||
{/if}
|
||||
<div class="flex items-center gap-1">
|
||||
{#if conv.documentMode}
|
||||
<span
|
||||
class="text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full"
|
||||
>
|
||||
Dokument
|
||||
</span>
|
||||
{/if}
|
||||
<!-- Action Buttons (visible on hover) -->
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onclick={(e) => handleArchive(e, conv.id)}
|
||||
class="p-1.5 text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/10 rounded-lg transition-colors"
|
||||
title="Archivieren"
|
||||
>
|
||||
<Archive size={14} weight="bold" />
|
||||
</button>
|
||||
<button
|
||||
onclick={(e) => handleDelete(e, conv.id)}
|
||||
class="p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
|
||||
title="Löschen"
|
||||
>
|
||||
<Trash size={14} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@
|
|||
{truncateTitle(conv.title || 'Neue Konversation')}
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground flex-shrink-0">
|
||||
{formatDate(conv.updated_at || conv.created_at)}
|
||||
{formatDate(conv.updatedAt || conv.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { marked } from 'marked';
|
||||
import type { Message } from '@chat/types';
|
||||
import { Robot, User } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
message: Message;
|
||||
|
|
@ -16,31 +17,216 @@
|
|||
gfm: true,
|
||||
});
|
||||
|
||||
const htmlContent = $derived(isUser ? message.message_text : marked.parse(message.message_text));
|
||||
const htmlContent = $derived(
|
||||
isUser ? message.messageText : marked.parse(message.messageText || '')
|
||||
);
|
||||
|
||||
const formattedTime = $derived(
|
||||
new Date(message.created_at).toLocaleTimeString('de-DE', {
|
||||
new Date(message.createdAt).toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex {isUser ? 'justify-end' : 'justify-start'} mb-4">
|
||||
<div class="group flex gap-3 {isUser ? 'flex-row-reverse' : 'flex-row'} mb-6 animate-fade-in">
|
||||
<!-- Avatar -->
|
||||
<div
|
||||
class="max-w-[80%] rounded-2xl px-4 py-3 {isUser
|
||||
? 'bg-primary text-primary-foreground rounded-br-md'
|
||||
: 'bg-muted text-foreground rounded-bl-md'}"
|
||||
class="flex-shrink-0 w-9 h-9 rounded-full flex items-center justify-center shadow-md
|
||||
{isUser
|
||||
? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white'
|
||||
: 'bg-gradient-to-br from-violet-500 to-purple-600 text-white'}"
|
||||
>
|
||||
{#if isUser}
|
||||
<p class="whitespace-pre-wrap">{message.message_text}</p>
|
||||
<User size={18} weight="bold" />
|
||||
{:else}
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
{@html htmlContent}
|
||||
</div>
|
||||
<Robot size={18} weight="bold" />
|
||||
{/if}
|
||||
<div class="text-xs mt-1 {isUser ? 'text-primary-foreground/70' : 'text-muted-foreground'}">
|
||||
{formattedTime}
|
||||
</div>
|
||||
|
||||
<!-- Message Content -->
|
||||
<div class="flex flex-col {isUser ? 'items-end' : 'items-start'} max-w-[75%]">
|
||||
<!-- Bubble -->
|
||||
<div
|
||||
class="relative px-4 py-3 shadow-md
|
||||
{isUser
|
||||
? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white rounded-2xl rounded-tr-md'
|
||||
: 'bg-white dark:bg-white/10 text-foreground border border-black/5 dark:border-white/10 rounded-2xl rounded-tl-md'}"
|
||||
>
|
||||
{#if isUser}
|
||||
<p class="whitespace-pre-wrap text-[15px] leading-relaxed">{message.messageText}</p>
|
||||
{:else}
|
||||
<div class="prose-chat">
|
||||
{@html htmlContent}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Time -->
|
||||
<div
|
||||
class="flex items-center gap-2 mt-1.5 px-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{formattedTime}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Fade-in animation */
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Markdown/Prose styling for assistant messages */
|
||||
:global(.prose-chat) {
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
:global(.prose-chat p) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:global(.prose-chat p + p) {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
:global(.prose-chat strong) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:global(.prose-chat em) {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
:global(.prose-chat ul),
|
||||
:global(.prose-chat ol) {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
:global(.prose-chat li) {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
:global(.prose-chat code) {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
:global(.dark .prose-chat code) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
:global(.prose-chat pre) {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 1em;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
|
||||
:global(.dark .prose-chat pre) {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:global(.prose-chat pre code) {
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
:global(.prose-chat blockquote) {
|
||||
border-left: 3px solid var(--color-primary, #3b82f6);
|
||||
padding-left: 1em;
|
||||
margin: 0.75em 0;
|
||||
color: var(--color-muted-foreground);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
:global(.prose-chat h1),
|
||||
:global(.prose-chat h2),
|
||||
:global(.prose-chat h3),
|
||||
:global(.prose-chat h4) {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
:global(.prose-chat h1) {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
:global(.prose-chat h2) {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
||||
:global(.prose-chat h3) {
|
||||
font-size: 1.05em;
|
||||
}
|
||||
|
||||
:global(.prose-chat a) {
|
||||
color: var(--color-primary, #3b82f6);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
:global(.prose-chat a:hover) {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:global(.prose-chat hr) {
|
||||
border: none;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
:global(.dark .prose-chat hr) {
|
||||
border-top-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
:global(.prose-chat table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0.75em 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
:global(.prose-chat th),
|
||||
:global(.prose-chat td) {
|
||||
padding: 0.5em 0.75em;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
:global(.dark .prose-chat th),
|
||||
:global(.dark .prose-chat td) {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
:global(.prose-chat th) {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:global(.dark .prose-chat th) {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -22,11 +22,11 @@
|
|||
value={selectedModelId}
|
||||
onchange={handleChange}
|
||||
{disabled}
|
||||
class="appearance-none bg-muted text-foreground
|
||||
text-sm rounded-lg px-3 py-2 pr-8 border border-border
|
||||
focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent
|
||||
class="appearance-none bg-white/80 dark:bg-white/10 backdrop-blur-xl text-foreground
|
||||
text-sm font-medium rounded-xl px-4 py-2.5 pr-9 border border-black/10 dark:border-white/20
|
||||
focus:outline-none focus:ring-2 focus:ring-primary/50
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
cursor-pointer min-w-[160px]"
|
||||
cursor-pointer min-w-[160px] shadow-md hover:shadow-lg transition-all"
|
||||
>
|
||||
{#if models.length === 0}
|
||||
<option value="">Laden...</option>
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<CaretDown size={16} weight="bold" class="text-muted-foreground" />
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<CaretDown size={14} weight="bold" class="text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,27 +1,56 @@
|
|||
<script lang="ts">
|
||||
// Typing indicator with animated dots
|
||||
import { Robot } from '@manacore/shared-icons';
|
||||
</script>
|
||||
|
||||
<div class="flex justify-start mb-4">
|
||||
<div class="bg-muted rounded-2xl rounded-bl-md px-4 py-3">
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
class="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce"
|
||||
style="animation-delay: 0ms"
|
||||
></div>
|
||||
<div
|
||||
class="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce"
|
||||
style="animation-delay: 150ms"
|
||||
></div>
|
||||
<div
|
||||
class="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce"
|
||||
style="animation-delay: 300ms"
|
||||
></div>
|
||||
<div class="flex gap-3 flex-row mb-6 animate-fade-in">
|
||||
<!-- Avatar -->
|
||||
<div
|
||||
class="flex-shrink-0 w-9 h-9 rounded-full flex items-center justify-center shadow-sm
|
||||
bg-gradient-to-br from-violet-500 to-purple-600 text-white"
|
||||
>
|
||||
<Robot size={18} weight="bold" />
|
||||
</div>
|
||||
|
||||
<!-- Typing Bubble -->
|
||||
<div class="flex flex-col items-start max-w-[75%]">
|
||||
<div
|
||||
class="relative px-4 py-3 shadow-sm
|
||||
bg-white dark:bg-white/10 text-foreground border border-black/5 dark:border-white/10 rounded-2xl rounded-tl-md"
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div
|
||||
class="w-2 h-2 bg-muted-foreground/60 rounded-full animate-bounce"
|
||||
style="animation-delay: 0ms"
|
||||
></div>
|
||||
<div
|
||||
class="w-2 h-2 bg-muted-foreground/60 rounded-full animate-bounce"
|
||||
style="animation-delay: 150ms"
|
||||
></div>
|
||||
<div
|
||||
class="w-2 h-2 bg-muted-foreground/60 rounded-full animate-bounce"
|
||||
style="animation-delay: 300ms"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
60%,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
{/if}
|
||||
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Erstellt: {formatDate(space.created_at)}
|
||||
Erstellt: {formatDate(space.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
<div
|
||||
class="group relative flex rounded-xl overflow-hidden bg-surface shadow-sm hover:shadow-md transition-all
|
||||
{template.is_default
|
||||
{template.isDefault
|
||||
? 'ring-2 ring-primary'
|
||||
: 'border border-border'}"
|
||||
>
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
<h3 class="text-base font-semibold text-foreground truncate">
|
||||
{template.name}
|
||||
</h3>
|
||||
{#if template.is_default}
|
||||
{#if template.isDefault}
|
||||
<span class="px-2 py-0.5 text-xs font-medium bg-primary text-primary-foreground rounded">
|
||||
Standard
|
||||
</span>
|
||||
|
|
@ -49,13 +49,13 @@
|
|||
{/if}
|
||||
|
||||
<p class="text-xs text-muted-foreground italic line-clamp-2">
|
||||
{truncatePrompt(template.system_prompt)}
|
||||
{truncatePrompt(template.systemPrompt)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{#if !template.is_default}
|
||||
{#if !template.isDefault}
|
||||
<button
|
||||
onclick={() => onSetDefault(template.id)}
|
||||
class="p-1.5 text-muted-foreground hover:text-yellow-500 hover:bg-muted rounded-lg transition-colors"
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@
|
|||
// 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 systemPrompt = $state(template?.systemPrompt ?? '');
|
||||
let initialQuestion = $state(template?.initialQuestion ?? '');
|
||||
let selectedColor = $state(template?.color ?? TEMPLATE_COLORS[0]);
|
||||
let selectedModelId = $state(template?.model_id ?? '');
|
||||
let documentMode = $state(template?.document_mode ?? false);
|
||||
let selectedModelId = $state(template?.modelId ?? '');
|
||||
let documentMode = $state(template?.documentMode ?? false);
|
||||
|
||||
// Models
|
||||
let models = $state<AIModel[]>([]);
|
||||
|
|
@ -67,11 +67,11 @@
|
|||
id: template?.id,
|
||||
name,
|
||||
description: description.trim() || null,
|
||||
system_prompt: systemPrompt,
|
||||
initial_question: initialQuestion.trim() || null,
|
||||
systemPrompt: systemPrompt,
|
||||
initialQuestion: initialQuestion.trim() || null,
|
||||
color: selectedColor,
|
||||
model_id: selectedModelId || null,
|
||||
document_mode: documentMode,
|
||||
modelId: selectedModelId || null,
|
||||
documentMode: documentMode,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -211,10 +211,10 @@ export type Template = {
|
|||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
description: string | null;
|
||||
systemPrompt: string;
|
||||
initialQuestion?: string;
|
||||
modelId?: string;
|
||||
initialQuestion: string | null;
|
||||
modelId: string | null;
|
||||
color: string;
|
||||
isDefault: boolean;
|
||||
documentMode: boolean;
|
||||
|
|
@ -585,7 +585,7 @@ export type Model = {
|
|||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
provider: string;
|
||||
provider: 'gemini' | 'azure' | 'openai';
|
||||
parameters?: {
|
||||
deployment?: string;
|
||||
temperature?: number;
|
||||
|
|
@ -593,6 +593,7 @@ export type Model = {
|
|||
top_p?: number;
|
||||
};
|
||||
isActive: boolean;
|
||||
isDefault: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { documentApi, conversationApi, type Document } from './api';
|
|||
export type { Document };
|
||||
|
||||
export type DocumentWithConversation = Document & {
|
||||
conversation_title: string;
|
||||
conversationTitle: string;
|
||||
};
|
||||
|
||||
export const documentService = {
|
||||
|
|
@ -35,7 +35,7 @@ export const documentService = {
|
|||
if (doc) {
|
||||
documents.push({
|
||||
...doc,
|
||||
conversation_title: conv.title || 'Unbenannte Konversation',
|
||||
conversationTitle: conv.title || 'Unbenannte Konversation',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export const spaceService = {
|
|||
async createSpace(space: {
|
||||
name: string;
|
||||
description?: string;
|
||||
owner_id: string;
|
||||
ownerId: string;
|
||||
}): Promise<string | null> {
|
||||
const result = await spaceApi.createSpace(space.name, space.description);
|
||||
return result?.id || null;
|
||||
|
|
|
|||
|
|
@ -71,10 +71,10 @@ export const chatStore = {
|
|||
// Add user message
|
||||
const userMessage: Message = {
|
||||
id: `temp-${++messageCounter}`,
|
||||
conversation_id: '',
|
||||
conversationId: '',
|
||||
sender: 'user',
|
||||
message_text: text,
|
||||
created_at: new Date().toISOString(),
|
||||
messageText: text,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
messages = [...messages, userMessage];
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ export const chatStore = {
|
|||
// Build chat messages for API
|
||||
const chatMessages: ChatMessage[] = messages.map((m) => ({
|
||||
role: m.sender === 'user' ? 'user' : 'assistant',
|
||||
content: m.message_text,
|
||||
content: m.messageText,
|
||||
}));
|
||||
|
||||
const request: ChatCompletionRequest = {
|
||||
|
|
@ -96,10 +96,10 @@ export const chatStore = {
|
|||
// Add assistant message
|
||||
const assistantMessage: Message = {
|
||||
id: `temp-${++messageCounter}`,
|
||||
conversation_id: '',
|
||||
conversationId: '',
|
||||
sender: 'assistant',
|
||||
message_text: response.content,
|
||||
created_at: new Date().toISOString(),
|
||||
messageText: response.content,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
messages = [...messages, assistantMessage];
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const conversationsStore = {
|
|||
const conversation = conversations.find((c) => c.id === conversationId);
|
||||
if (conversation) {
|
||||
conversations = conversations.filter((c) => c.id !== conversationId);
|
||||
archivedConversations = [{ ...conversation, is_archived: true }, ...archivedConversations];
|
||||
archivedConversations = [{ ...conversation, isArchived: true }, ...archivedConversations];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ export const conversationsStore = {
|
|||
const conversation = archivedConversations.find((c) => c.id === conversationId);
|
||||
if (conversation) {
|
||||
archivedConversations = archivedConversations.filter((c) => c.id !== conversationId);
|
||||
conversations = [{ ...conversation, is_archived: false }, ...conversations];
|
||||
conversations = [{ ...conversation, isArchived: false }, ...conversations];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export const spacesStore = {
|
|||
const spaceId = await spaceService.createSpace(space);
|
||||
if (spaceId) {
|
||||
// Reload spaces to get the new one with full data
|
||||
await this.loadSpaces(space.owner_id);
|
||||
await this.loadSpaces(space.ownerId);
|
||||
}
|
||||
return spaceId;
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export const templatesStore = {
|
|||
if (success) {
|
||||
templates = templates.map((t) => ({
|
||||
...t,
|
||||
is_default: t.id === templateId,
|
||||
isDefault: t.id === templateId,
|
||||
}));
|
||||
}
|
||||
return success;
|
||||
|
|
|
|||
|
|
@ -119,13 +119,13 @@
|
|||
{conv.title || 'Unbenannte Konversation'}
|
||||
</h3>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground">{formatDate(conv.updated_at)}</span>
|
||||
<span class="text-xs text-muted-foreground">{formatDate(conv.updatedAt)}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span class="px-2 py-0.5 bg-muted rounded">
|
||||
{conv.conversation_mode === 'free'
|
||||
{conv.conversationMode === 'free'
|
||||
? 'Freier Modus'
|
||||
: conv.conversation_mode === 'guided'
|
||||
: conv.conversationMode === 'guided'
|
||||
? 'Geführter Modus'
|
||||
: 'Vorlagen-Modus'}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@
|
|||
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 ChatLayout from '$lib/components/chat/ChatLayout.svelte';
|
||||
import type { AIModel, Message, Template } from '@chat/types';
|
||||
import { FileText, Sparkle } from '@manacore/shared-icons';
|
||||
import { Sparkle } from '@manacore/shared-icons';
|
||||
|
||||
let models = $state<AIModel[]>([]);
|
||||
let templates = $state<Template[]>([]);
|
||||
|
|
@ -157,7 +156,7 @@
|
|||
</p>
|
||||
|
||||
<!-- Suggestion Pills -->
|
||||
<div class="flex flex-wrap justify-center gap-3 mb-8">
|
||||
<div class="flex flex-wrap justify-center gap-3">
|
||||
<button
|
||||
onclick={() => handleSend('Erkläre mir, wie KI funktioniert')}
|
||||
class="px-5 py-2.5 text-sm font-medium rounded-full bg-white/80 dark:bg-white/10 backdrop-blur-xl border border-black/10 dark:border-white/20 text-foreground shadow-md hover:shadow-lg hover:bg-white dark:hover:bg-white/20 transition-all duration-200 hover:-translate-y-0.5"
|
||||
|
|
@ -177,101 +176,6 @@
|
|||
Tech-Trends
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Options Bar -->
|
||||
<div
|
||||
class="inline-flex items-center gap-2 p-1.5 rounded-full bg-white/70 dark:bg-white/10 backdrop-blur-xl border border-black/10 dark:border-white/20 shadow-md"
|
||||
>
|
||||
<!-- Model Selector -->
|
||||
<div class="relative">
|
||||
<select
|
||||
value={selectedModelId}
|
||||
onchange={(e) => handleModelSelect((e.target as HTMLSelectElement).value)}
|
||||
disabled={isSending}
|
||||
class="appearance-none bg-transparent text-foreground text-sm font-medium rounded-full pl-4 pr-8 py-2 border-0 focus:outline-none focus:ring-0 disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
{#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-2 flex items-center pointer-events-none"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
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>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="w-px h-6 bg-black/10 dark:bg-white/20"></div>
|
||||
|
||||
<!-- Template Selector -->
|
||||
{#if templates.length > 0}
|
||||
<div class="relative">
|
||||
<select
|
||||
onchange={handleTemplateSelect}
|
||||
value={selectedTemplateId}
|
||||
disabled={isSending}
|
||||
class="appearance-none bg-transparent text-foreground text-sm font-medium rounded-full pl-4 pr-8 py-2 border-0 focus:outline-none focus:ring-0 disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
<option value="">Ohne Vorlage</option>
|
||||
{#each templates as template}
|
||||
<option value={template.id}>
|
||||
{template.name}
|
||||
{template.isDefault ? ' ★' : ''}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<div
|
||||
class="absolute inset-y-0 right-2 flex items-center pointer-events-none"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
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>
|
||||
<div class="w-px h-6 bg-black/10 dark:bg-white/20"></div>
|
||||
{/if}
|
||||
|
||||
<!-- Document Mode Toggle -->
|
||||
<button
|
||||
onclick={toggleDocumentMode}
|
||||
disabled={isSending}
|
||||
class="flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-full transition-all duration-200 disabled:opacity-50
|
||||
{documentMode
|
||||
? 'bg-primary/20 text-primary'
|
||||
: 'text-foreground hover:bg-black/5 dark:hover:bg-white/10'}"
|
||||
title="Dokumentmodus aktivieren"
|
||||
>
|
||||
<FileText size={16} weight={documentMode ? 'fill' : 'bold'} />
|
||||
<span>Dokument</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
@ -284,7 +188,15 @@
|
|||
<!-- Floating Chat Input -->
|
||||
<div class="flex-shrink-0 p-4 bg-gradient-to-t from-background via-background to-transparent">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<ChatInput onSend={handleSend} disabled={isSending || isLoading} />
|
||||
<ChatInput
|
||||
onSend={handleSend}
|
||||
disabled={isSending || isLoading}
|
||||
{models}
|
||||
{selectedModelId}
|
||||
onModelSelect={handleModelSelect}
|
||||
{documentMode}
|
||||
onDocumentModeToggle={toggleDocumentMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,19 @@
|
|||
<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 MessageList from '$lib/components/chat/MessageList.svelte';
|
||||
import ChatInput from '$lib/components/chat/ChatInput.svelte';
|
||||
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
|
||||
import ChatLayout from '$lib/components/chat/ChatLayout.svelte';
|
||||
import type { Conversation, Message, AIModel, Document } from '@chat/types';
|
||||
import {
|
||||
Archive,
|
||||
Trash,
|
||||
FileText,
|
||||
ClockCounterClockwise,
|
||||
X,
|
||||
FloppyDisk,
|
||||
ChatCircle,
|
||||
} from '@manacore/shared-icons';
|
||||
|
||||
let conversation = $state<Conversation | null>(null);
|
||||
|
|
@ -39,7 +33,7 @@
|
|||
let showDocumentPanel = $state(true);
|
||||
|
||||
const conversationId = $derived($page.params.id ?? '');
|
||||
const isDocumentMode = $derived(conversation?.document_mode ?? false);
|
||||
const isDocumentMode = $derived(conversation?.documentMode ?? false);
|
||||
|
||||
onMount(async () => {
|
||||
await loadData();
|
||||
|
|
@ -62,13 +56,13 @@
|
|||
}
|
||||
|
||||
// Set model from conversation
|
||||
selectedModelId = conversation.model_id;
|
||||
selectedModelId = conversation.modelId;
|
||||
|
||||
// Load messages
|
||||
messages = await conversationService.getMessages(conversationId);
|
||||
|
||||
// Load document if in document mode
|
||||
if (conversation.document_mode) {
|
||||
if (conversation.documentMode) {
|
||||
document = await documentService.getLatestDocument(conversationId);
|
||||
documentContent = document?.content ?? '';
|
||||
}
|
||||
|
|
@ -121,10 +115,10 @@
|
|||
// Optimistic update - add user message
|
||||
const tempUserMessage: Message = {
|
||||
id: `temp-user-${Date.now()}`,
|
||||
conversation_id: conversationId,
|
||||
conversationId: conversationId,
|
||||
sender: 'user',
|
||||
message_text: text,
|
||||
created_at: new Date().toISOString(),
|
||||
messageText: text,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
messages = [...messages, tempUserMessage];
|
||||
|
||||
|
|
@ -155,26 +149,6 @@
|
|||
function handleModelSelect(modelId: string) {
|
||||
selectedModelId = modelId;
|
||||
}
|
||||
|
||||
async function handleArchive() {
|
||||
if (!conversation) return;
|
||||
|
||||
const success = await conversationsStore.archiveConversation(conversationId);
|
||||
if (success) {
|
||||
goto('/chat');
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -196,70 +170,6 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Chat Header -->
|
||||
<header
|
||||
class="flex-shrink-0 border-b border-border bg-surface/50 backdrop-blur-sm px-6 py-4"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-primary/60 flex items-center justify-center"
|
||||
>
|
||||
<ChatCircle size={22} weight="fill" class="text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-foreground truncate max-w-xs">
|
||||
{conversation?.title || 'Chat'}
|
||||
</h2>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{messages.length} Nachricht{messages.length !== 1 ? 'en' : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Model Selector -->
|
||||
<ModelSelector
|
||||
{models}
|
||||
{selectedModelId}
|
||||
onSelect={handleModelSelect}
|
||||
disabled={isSending}
|
||||
/>
|
||||
|
||||
{#if isDocumentMode}
|
||||
<button
|
||||
onclick={toggleDocumentPanel}
|
||||
class="p-2.5 transition-colors rounded-xl
|
||||
{showDocumentPanel
|
||||
? 'text-primary bg-primary/10 border border-primary/30'
|
||||
: 'text-muted-foreground bg-muted border border-border hover:bg-muted/80'}"
|
||||
aria-label="Dokument-Panel"
|
||||
title="Dokument-Panel ein/ausblenden"
|
||||
>
|
||||
<FileText size={18} weight="bold" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
onclick={handleArchive}
|
||||
class="p-2.5 text-muted-foreground bg-muted border border-border rounded-xl hover:bg-muted/80 transition-colors"
|
||||
aria-label="Archivieren"
|
||||
title="Archivieren"
|
||||
>
|
||||
<Archive size={18} weight="bold" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={handleDelete}
|
||||
class="p-2.5 text-destructive bg-muted border border-border rounded-xl hover:bg-destructive/10 transition-colors"
|
||||
aria-label="Löschen"
|
||||
title="Löschen"
|
||||
>
|
||||
<Trash size={18} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- Chat Area -->
|
||||
|
|
@ -277,10 +187,17 @@
|
|||
|
||||
<!-- Floating Chat Input -->
|
||||
<div
|
||||
class="flex-shrink-0 p-4 bg-gradient-to-t from-surface via-surface to-transparent"
|
||||
class="flex-shrink-0 p-4 bg-gradient-to-t from-background via-background to-transparent"
|
||||
>
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<ChatInput onSend={handleSend} disabled={isSending} />
|
||||
<ChatInput
|
||||
onSend={handleSend}
|
||||
disabled={isSending}
|
||||
{models}
|
||||
{selectedModelId}
|
||||
onModelSelect={handleModelSelect}
|
||||
documentMode={isDocumentMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -399,7 +316,7 @@ Du kannst Markdown verwenden:
|
|||
{version.id === document?.id ? ' (aktuell)' : ''}
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{new Date(version.created_at).toLocaleDateString('de-DE', {
|
||||
{new Date(version.createdAt).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@
|
|||
<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)}
|
||||
onclick={() => navigateToConversation(doc.conversationId)}
|
||||
class="text-left p-0 bg-surface rounded-xl border border-border
|
||||
shadow-sm hover:shadow-md hover:border-primary/50 transition-all overflow-hidden"
|
||||
>
|
||||
|
|
@ -149,9 +149,9 @@
|
|||
{extractTitle(doc.content)}
|
||||
</h3>
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span class="truncate">{doc.conversation_title}</span>
|
||||
<span class="truncate">{doc.conversationTitle}</span>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<span>{formatDate(doc.updated_at)}</span>
|
||||
<span>{formatDate(doc.updatedAt)}</span>
|
||||
<span class="px-1.5 py-0.5 bg-muted rounded font-medium">
|
||||
v{doc.version}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@
|
|||
await spacesStore.createSpace({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
owner_id: authStore.user.id,
|
||||
ownerId: authStore.user.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
}
|
||||
|
||||
function isOwner(space: Space): boolean {
|
||||
return space.owner_id === authStore.user?.id;
|
||||
return space.ownerId === authStore.user?.id;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@
|
|||
<h3 class="font-medium text-foreground">
|
||||
{conv.title || 'Neue Konversation'}
|
||||
</h3>
|
||||
<span class="text-xs text-muted-foreground">{formatDate(conv.updated_at)}</span>
|
||||
<span class="text-xs text-muted-foreground">{formatDate(conv.updatedAt)}</span>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -50,10 +50,10 @@
|
|||
// Create a new conversation with this template
|
||||
const conversationId = await conversationService.createConversation(
|
||||
authStore.user.id,
|
||||
template.model_id || '550e8400-e29b-41d4-a716-446655440101', // Default to Gemini 2.5 Flash
|
||||
template.modelId || '550e8400-e29b-41d4-a716-446655440101', // Default to Gemini 2.5 Flash
|
||||
'template',
|
||||
template.id,
|
||||
template.document_mode
|
||||
template.documentMode
|
||||
);
|
||||
|
||||
if (conversationId) {
|
||||
|
|
@ -70,24 +70,24 @@
|
|||
await templatesStore.updateTemplate(data.id, {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
system_prompt: data.system_prompt,
|
||||
initial_question: data.initial_question,
|
||||
systemPrompt: data.systemPrompt,
|
||||
initialQuestion: data.initialQuestion,
|
||||
color: data.color,
|
||||
model_id: data.model_id,
|
||||
document_mode: data.document_mode,
|
||||
modelId: data.modelId,
|
||||
documentMode: data.documentMode,
|
||||
});
|
||||
} else {
|
||||
// Create new template
|
||||
await templatesStore.createTemplate({
|
||||
user_id: authStore.user.id,
|
||||
userId: authStore.user.id,
|
||||
name: data.name!,
|
||||
description: data.description ?? null,
|
||||
system_prompt: data.system_prompt!,
|
||||
initial_question: data.initial_question ?? null,
|
||||
systemPrompt: data.systemPrompt!,
|
||||
initialQuestion: data.initialQuestion ?? null,
|
||||
color: data.color!,
|
||||
model_id: data.model_id ?? null,
|
||||
is_default: false,
|
||||
document_mode: data.document_mode ?? false,
|
||||
modelId: data.modelId ?? null,
|
||||
isDefault: false,
|
||||
documentMode: data.documentMode ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,42 +10,46 @@ export interface ChatMessage {
|
|||
|
||||
export interface Message {
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
conversationId: string;
|
||||
sender: 'user' | 'assistant' | 'system';
|
||||
message_text: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
messageText: string;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
// Conversation Types
|
||||
export interface Conversation {
|
||||
id: string;
|
||||
user_id: string;
|
||||
model_id: string;
|
||||
template_id?: string;
|
||||
conversation_mode: 'free' | 'guided' | 'template';
|
||||
document_mode: boolean;
|
||||
userId: string;
|
||||
modelId: string;
|
||||
templateId?: string;
|
||||
spaceId?: string;
|
||||
conversationMode: 'free' | 'guided' | 'template';
|
||||
documentMode: boolean;
|
||||
title?: string;
|
||||
is_archived: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
isArchived: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// AI Model Types
|
||||
export interface AIModelParameters {
|
||||
model?: string;
|
||||
temperature: number;
|
||||
max_tokens: number;
|
||||
provider: 'azure' | 'openai';
|
||||
deployment: string;
|
||||
endpoint: string;
|
||||
api_version: string;
|
||||
maxTokens?: number;
|
||||
max_tokens?: number; // Legacy support
|
||||
}
|
||||
|
||||
export interface AIModel {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
provider: 'gemini' | 'azure' | 'openai';
|
||||
parameters: AIModelParameters;
|
||||
isActive: boolean;
|
||||
isDefault: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
// Token Usage Types
|
||||
|
|
@ -64,22 +68,22 @@ export interface ChatCompletionResponse {
|
|||
// Template Types
|
||||
export interface Template {
|
||||
id: string;
|
||||
user_id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
system_prompt: string;
|
||||
initial_question: string | null;
|
||||
model_id: string | null;
|
||||
systemPrompt: string;
|
||||
initialQuestion: string | null;
|
||||
modelId: string | null;
|
||||
color: string;
|
||||
is_default: boolean;
|
||||
document_mode: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
isDefault: boolean;
|
||||
documentMode: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type TemplateCreate = Omit<Template, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type TemplateCreate = Omit<Template, 'id' | 'createdAt' | 'updatedAt'>;
|
||||
export type TemplateUpdate = Partial<
|
||||
Omit<Template, 'id' | 'user_id' | 'created_at' | 'updated_at'>
|
||||
Omit<Template, 'id' | 'userId' | 'createdAt' | 'updatedAt'>
|
||||
>;
|
||||
|
||||
// Space Types
|
||||
|
|
@ -87,38 +91,38 @@ export interface Space {
|
|||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
owner_id: string;
|
||||
is_archived: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
ownerId: string;
|
||||
isArchived: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type SpaceCreate = Pick<Space, 'name' | 'description' | 'owner_id'>;
|
||||
export type SpaceUpdate = Partial<Pick<Space, 'name' | 'description' | 'is_archived'>>;
|
||||
export type SpaceCreate = Pick<Space, 'name' | 'description' | 'ownerId'>;
|
||||
export type SpaceUpdate = Partial<Pick<Space, 'name' | 'description' | 'isArchived'>>;
|
||||
|
||||
export interface SpaceMember {
|
||||
id: string;
|
||||
space_id: string;
|
||||
user_id: string;
|
||||
spaceId: string;
|
||||
userId: string;
|
||||
role: 'owner' | 'admin' | 'member' | 'viewer';
|
||||
invitation_status: 'pending' | 'accepted' | 'declined';
|
||||
invited_by?: string;
|
||||
invited_at: string;
|
||||
joined_at?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
invitationStatus: 'pending' | 'accepted' | 'declined';
|
||||
invitedBy?: string;
|
||||
invitedAt: string;
|
||||
joinedAt?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// Document Types
|
||||
export interface Document {
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
conversationId: string;
|
||||
content: string;
|
||||
version: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface DocumentWithConversation extends Document {
|
||||
conversation_title: string;
|
||||
conversationTitle: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue