mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 19:06:41 +02:00
feat(chat): add auto title generation, inline renaming, and styled delete modal
- Fix missing conversationsStore import for auto title generation - Make model ID dynamic in generateTitle() with error handling and fallback - Add inline editing for manual conversation renaming in sidebar - Add updateConversationTitle API endpoint and store method - Replace browser confirm() with styled ConfirmationModal for delete - Update Modal and ConfirmationModal with glassmorphism styling - Add DEV_BYPASS_AUTH and GOOGLE_GENAI_API_KEY to env generation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a32c4f0a16
commit
05fe8ca5b6
14 changed files with 447 additions and 71 deletions
|
|
@ -165,6 +165,34 @@ export class ConversationController {
|
|||
return result.value;
|
||||
}
|
||||
|
||||
@Patch(':id/pin')
|
||||
async pinConversation(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: CurrentUserData
|
||||
): Promise<Conversation> {
|
||||
const result = await this.conversationService.pinConversation(id, user.userId);
|
||||
|
||||
if (!isOk(result)) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
@Patch(':id/unpin')
|
||||
async unpinConversation(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: CurrentUserData
|
||||
): Promise<Conversation> {
|
||||
const result = await this.conversationService.unpinConversation(id, user.userId);
|
||||
|
||||
if (!isOk(result)) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async deleteConversation(
|
||||
@Param('id') id: string,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export class ConversationService {
|
|||
.select()
|
||||
.from(conversations)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(conversations.updatedAt));
|
||||
.orderBy(desc(conversations.isPinned), desc(conversations.updatedAt));
|
||||
|
||||
return ok(result);
|
||||
} catch (error) {
|
||||
|
|
@ -267,4 +267,46 @@ export class ConversationService {
|
|||
return err(DatabaseError.queryFailed('Failed to get message count'));
|
||||
}
|
||||
}
|
||||
|
||||
async pinConversation(conversationId: string, userId: string): AsyncResult<Conversation> {
|
||||
try {
|
||||
// First verify the conversation belongs to the user
|
||||
const convResult = await this.getConversation(conversationId, userId);
|
||||
if (!convResult.ok) {
|
||||
return err(convResult.error);
|
||||
}
|
||||
|
||||
const result = await this.db
|
||||
.update(conversations)
|
||||
.set({ isPinned: true, updatedAt: new Date() })
|
||||
.where(eq(conversations.id, conversationId))
|
||||
.returning();
|
||||
|
||||
return ok(result[0]);
|
||||
} catch (error) {
|
||||
this.logger.error('Error pinning conversation', error);
|
||||
return err(DatabaseError.queryFailed('Failed to pin conversation'));
|
||||
}
|
||||
}
|
||||
|
||||
async unpinConversation(conversationId: string, userId: string): AsyncResult<Conversation> {
|
||||
try {
|
||||
// First verify the conversation belongs to the user
|
||||
const convResult = await this.getConversation(conversationId, userId);
|
||||
if (!convResult.ok) {
|
||||
return err(convResult.error);
|
||||
}
|
||||
|
||||
const result = await this.db
|
||||
.update(conversations)
|
||||
.set({ isPinned: false, updatedAt: new Date() })
|
||||
.where(eq(conversations.id, conversationId))
|
||||
.returning();
|
||||
|
||||
return ok(result[0]);
|
||||
} catch (error) {
|
||||
this.logger.error('Error unpinning conversation', error);
|
||||
return err(DatabaseError.queryFailed('Failed to unpin conversation'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export const conversations = pgTable('conversations', {
|
|||
conversationMode: conversationModeEnum('conversation_mode').default('free').notNull(),
|
||||
documentMode: boolean('document_mode').default(false).notNull(),
|
||||
isArchived: boolean('is_archived').default(false).notNull(),
|
||||
isPinned: boolean('is_pinned').default(false).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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, Archive, Trash } from '@manacore/shared-icons';
|
||||
import { MagnifyingGlass, X, Plus, ChatCircle, Archive, Trash, PushPin } from '@manacore/shared-icons';
|
||||
import { ConfirmationModal } from '@manacore/shared-ui';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
|
|
@ -14,6 +15,11 @@
|
|||
|
||||
let { children }: Props = $props();
|
||||
|
||||
// Delete confirmation modal state
|
||||
let showDeleteModal = $state(false);
|
||||
let deleteTargetId = $state<string | null>(null);
|
||||
let isDeleting = $state(false);
|
||||
|
||||
// Resizer state
|
||||
let leftColumnWidth = $state(320);
|
||||
let isResizing = $state(false);
|
||||
|
|
@ -106,17 +112,48 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Delete conversation
|
||||
async function handleDelete(e: MouseEvent, convId: string) {
|
||||
// Pin/unpin conversation
|
||||
async function handleTogglePin(e: MouseEvent, convId: string, isPinned: boolean) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (confirm('Möchtest du diese Konversation wirklich löschen?')) {
|
||||
const success = await conversationsStore.deleteConversation(convId);
|
||||
if (success && isActive(convId)) {
|
||||
if (isPinned) {
|
||||
await conversationsStore.unpinConversation(convId);
|
||||
} else {
|
||||
await conversationsStore.pinConversation(convId);
|
||||
}
|
||||
}
|
||||
|
||||
// Open delete confirmation modal
|
||||
function handleDelete(e: MouseEvent, convId: string) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
deleteTargetId = convId;
|
||||
showDeleteModal = true;
|
||||
}
|
||||
|
||||
// Confirm delete action
|
||||
async function confirmDelete() {
|
||||
if (!deleteTargetId) return;
|
||||
|
||||
isDeleting = true;
|
||||
try {
|
||||
const wasActive = isActive(deleteTargetId);
|
||||
const success = await conversationsStore.deleteConversation(deleteTargetId);
|
||||
if (success && wasActive) {
|
||||
goto('/chat');
|
||||
}
|
||||
} finally {
|
||||
isDeleting = false;
|
||||
showDeleteModal = false;
|
||||
deleteTargetId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Close delete modal
|
||||
function closeDeleteModal() {
|
||||
showDeleteModal = false;
|
||||
deleteTargetId = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
|
@ -213,13 +250,21 @@
|
|||
>
|
||||
<!-- Title Row -->
|
||||
<div class="mb-1.5 flex items-center gap-2">
|
||||
<ChatCircle
|
||||
size={16}
|
||||
weight={isActive(conv.id) ? 'fill' : 'regular'}
|
||||
class="flex-shrink-0 {isActive(conv.id)
|
||||
? 'text-primary'
|
||||
: 'text-muted-foreground'}"
|
||||
/>
|
||||
{#if conv.isPinned}
|
||||
<PushPin
|
||||
size={16}
|
||||
weight="fill"
|
||||
class="flex-shrink-0 text-primary"
|
||||
/>
|
||||
{:else}
|
||||
<ChatCircle
|
||||
size={16}
|
||||
weight={isActive(conv.id) ? 'fill' : 'regular'}
|
||||
class="flex-shrink-0 {isActive(conv.id)
|
||||
? 'text-primary'
|
||||
: 'text-muted-foreground'}"
|
||||
/>
|
||||
{/if}
|
||||
<h3 class="text-sm font-semibold line-clamp-1 text-foreground flex-1">
|
||||
{conv.title || 'Neue Konversation'}
|
||||
</h3>
|
||||
|
|
@ -245,6 +290,13 @@
|
|||
{/if}
|
||||
<!-- Action Buttons (visible on hover) -->
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onclick={(e) => handleTogglePin(e, conv.id, conv.isPinned)}
|
||||
class="p-1.5 text-muted-foreground hover:text-primary hover:bg-primary/10 rounded-lg transition-colors {conv.isPinned ? 'text-primary' : ''}"
|
||||
title={conv.isPinned ? 'Nicht mehr anpinnen' : 'Anpinnen'}
|
||||
>
|
||||
<PushPin size={14} weight={conv.isPinned ? 'fill' : 'bold'} />
|
||||
</button>
|
||||
<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"
|
||||
|
|
@ -284,6 +336,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<ConfirmationModal
|
||||
visible={showDeleteModal}
|
||||
onClose={closeDeleteModal}
|
||||
onConfirm={confirmDelete}
|
||||
variant="danger"
|
||||
title="Konversation löschen?"
|
||||
message="Diese Aktion kann nicht rückgängig gemacht werden. Alle Nachrichten werden dauerhaft gelöscht."
|
||||
confirmLabel="Löschen"
|
||||
cancelLabel="Abbrechen"
|
||||
loading={isDeleting}
|
||||
/>
|
||||
|
||||
<style>
|
||||
/* Hide scrollbar completely */
|
||||
.scrollbar-hide {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { page } from '$app/stores';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import type { Conversation } from '@chat/types';
|
||||
import { Plus } from '@manacore/shared-icons';
|
||||
import { Plus, PencilSimple, Check, X } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
conversations: Conversation[];
|
||||
|
|
@ -11,6 +11,11 @@
|
|||
|
||||
let { conversations, isLoading = false }: Props = $props();
|
||||
|
||||
// Edit state
|
||||
let editingId = $state<string | null>(null);
|
||||
let editTitle = $state('');
|
||||
let isSaving = $state(false);
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
|
|
@ -32,6 +37,40 @@
|
|||
if (title.length <= maxLength) return title;
|
||||
return title.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
|
||||
function startEdit(conv: Conversation, event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
editingId = conv.id;
|
||||
editTitle = conv.title || '';
|
||||
}
|
||||
|
||||
async function saveTitle(conversationId: string) {
|
||||
if (!editTitle.trim() || isSaving) return;
|
||||
|
||||
isSaving = true;
|
||||
try {
|
||||
await conversationsStore.updateConversationTitle(conversationId, editTitle.trim());
|
||||
} finally {
|
||||
isSaving = false;
|
||||
editingId = null;
|
||||
editTitle = '';
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editingId = null;
|
||||
editTitle = '';
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent, conversationId: string) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
saveTitle(conversationId);
|
||||
} else if (event.key === 'Escape') {
|
||||
cancelEdit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
|
|
@ -65,22 +104,65 @@
|
|||
<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-primary/10 text-primary'
|
||||
: 'hover:bg-muted text-foreground'}"
|
||||
>
|
||||
<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-muted-foreground flex-shrink-0">
|
||||
{formatDate(conv.updatedAt || conv.createdAt)}
|
||||
</span>
|
||||
{#if editingId === conv.id}
|
||||
<!-- Edit Mode -->
|
||||
<div class="flex items-center gap-1 px-3 py-2 mx-2">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editTitle}
|
||||
onkeydown={(e) => handleKeydown(e, conv.id)}
|
||||
class="flex-1 px-2 py-1 text-sm bg-background border border-border rounded-md
|
||||
focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
autofocus
|
||||
/>
|
||||
<button
|
||||
onclick={() => saveTitle(conv.id)}
|
||||
disabled={isSaving || !editTitle.trim()}
|
||||
class="p-1.5 text-primary hover:bg-primary/10 rounded-md transition-colors
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Speichern"
|
||||
>
|
||||
<Check size={16} weight="bold" />
|
||||
</button>
|
||||
<button
|
||||
onclick={cancelEdit}
|
||||
class="p-1.5 text-muted-foreground hover:bg-muted rounded-md transition-colors"
|
||||
title="Abbrechen"
|
||||
>
|
||||
<X size={16} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
{:else}
|
||||
<!-- View Mode -->
|
||||
<div class="group relative">
|
||||
<a
|
||||
href="/chat/{conv.id}"
|
||||
class="block px-3 py-2 mx-2 rounded-lg transition-colors
|
||||
{isActive
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'hover:bg-muted text-foreground'}"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-sm font-medium truncate pr-6">
|
||||
{truncateTitle(conv.title || 'Neue Konversation')}
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground flex-shrink-0">
|
||||
{formatDate(conv.updatedAt || conv.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Edit Button (visible on hover) -->
|
||||
<button
|
||||
onclick={(e) => startEdit(conv, e)}
|
||||
class="absolute right-4 top-1/2 -translate-y-1/2 p-1 rounded-md
|
||||
text-muted-foreground hover:text-foreground hover:bg-muted
|
||||
opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
title="Umbenennen"
|
||||
>
|
||||
<PencilSimple size={14} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ export type Conversation = {
|
|||
conversationMode: 'free' | 'guided' | 'template';
|
||||
documentMode: boolean;
|
||||
isArchived: boolean;
|
||||
isPinned: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
|
@ -203,6 +204,28 @@ export const conversationApi = {
|
|||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
async pinConversation(conversationId: string): Promise<boolean> {
|
||||
const { error } = await fetchApi<Conversation>(`/conversations/${conversationId}/pin`, {
|
||||
method: 'PATCH',
|
||||
});
|
||||
if (error) {
|
||||
console.error('Error pinning conversation:', error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
async unpinConversation(conversationId: string): Promise<boolean> {
|
||||
const { error } = await fetchApi<Conversation>(`/conversations/${conversationId}/unpin`, {
|
||||
method: 'PATCH',
|
||||
});
|
||||
if (error) {
|
||||
console.error('Error unpinning conversation:', error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
// ============ Template API ============
|
||||
|
|
|
|||
|
|
@ -97,6 +97,20 @@ export const conversationService = {
|
|||
return conversationApi.deleteConversation(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Pin a conversation
|
||||
*/
|
||||
async pinConversation(conversationId: string): Promise<boolean> {
|
||||
return conversationApi.pinConversation(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Unpin a conversation
|
||||
*/
|
||||
async unpinConversation(conversationId: string): Promise<boolean> {
|
||||
return conversationApi.unpinConversation(conversationId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a message and get AI response
|
||||
*/
|
||||
|
|
@ -142,7 +156,7 @@ export const conversationService = {
|
|||
// 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);
|
||||
title = await this.generateTitle(userMessage, modelId);
|
||||
if (title) {
|
||||
await this.updateTitle(conversationId, title);
|
||||
}
|
||||
|
|
@ -157,32 +171,50 @@ export const conversationService = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Generate a conversation title based on user message
|
||||
* Generate a conversation title based on user message using AI
|
||||
*/
|
||||
async generateTitle(userMessage: string): Promise<string> {
|
||||
const titlePrompt = `Schreibe eine kurze, prägnante Überschrift (maximal 5 Wörter) für diesen Chat: "${userMessage}"`;
|
||||
async generateTitle(userMessage: string, modelId: string): Promise<string> {
|
||||
try {
|
||||
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-446655440101', // Gemini 2.5 Flash (default)
|
||||
temperature: 0.3,
|
||||
maxTokens: 50,
|
||||
});
|
||||
const response = await chatApi.createCompletion({
|
||||
messages: [{ role: 'user', content: titlePrompt }],
|
||||
modelId,
|
||||
temperature: 0.3,
|
||||
maxTokens: 50,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
return 'Neue Konversation';
|
||||
if (!response) {
|
||||
console.warn('Title generation returned no response, using fallback');
|
||||
return this.createFallbackTitle(userMessage);
|
||||
}
|
||||
|
||||
// Clean up title
|
||||
let title = response.content
|
||||
.trim()
|
||||
.replace(/^["']|["']$/g, '')
|
||||
.replace(/\.$/g, '');
|
||||
|
||||
if (title.length > 100) {
|
||||
title = title.substring(0, 97) + '...';
|
||||
}
|
||||
|
||||
return title || this.createFallbackTitle(userMessage);
|
||||
} catch (error) {
|
||||
console.error('Error generating title:', error);
|
||||
return this.createFallbackTitle(userMessage);
|
||||
}
|
||||
},
|
||||
|
||||
// Clean up title
|
||||
let title = response.content
|
||||
.trim()
|
||||
.replace(/^["']|["']$/g, '')
|
||||
.replace(/\.$/g, '');
|
||||
|
||||
if (title.length > 100) {
|
||||
title = title.substring(0, 97) + '...';
|
||||
/**
|
||||
* Create a fallback title from the first words of the message
|
||||
*/
|
||||
createFallbackTitle(message: string): string {
|
||||
const words = message.trim().split(/\s+/).slice(0, 5);
|
||||
let title = words.join(' ');
|
||||
if (message.trim().split(/\s+/).length > 5) {
|
||||
title += '...';
|
||||
}
|
||||
|
||||
return title;
|
||||
return title || 'Neue Konversation';
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -74,6 +74,17 @@ export const conversationsStore = {
|
|||
conversations = conversations.map((c) => (c.id === conversationId ? { ...c, ...updates } : c));
|
||||
},
|
||||
|
||||
/**
|
||||
* Update conversation title via API and update local state
|
||||
*/
|
||||
async updateConversationTitle(conversationId: string, title: string): Promise<boolean> {
|
||||
const success = await conversationService.updateTitle(conversationId, title);
|
||||
if (success) {
|
||||
this.updateConversation(conversationId, { title });
|
||||
}
|
||||
return success;
|
||||
},
|
||||
|
||||
/**
|
||||
* Archive a conversation
|
||||
*/
|
||||
|
|
@ -122,6 +133,48 @@ export const conversationsStore = {
|
|||
return success;
|
||||
},
|
||||
|
||||
/**
|
||||
* Pin a conversation (moves it to top of list)
|
||||
*/
|
||||
async pinConversation(conversationId: string) {
|
||||
const success = await conversationService.pinConversation(conversationId);
|
||||
|
||||
if (success) {
|
||||
conversations = conversations.map((c) =>
|
||||
c.id === conversationId ? { ...c, isPinned: true } : c
|
||||
);
|
||||
// Re-sort: pinned first, then by updatedAt
|
||||
conversations = [...conversations].sort((a, b) => {
|
||||
if (a.isPinned && !b.isPinned) return -1;
|
||||
if (!a.isPinned && b.isPinned) return 1;
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
return success;
|
||||
},
|
||||
|
||||
/**
|
||||
* Unpin a conversation
|
||||
*/
|
||||
async unpinConversation(conversationId: string) {
|
||||
const success = await conversationService.unpinConversation(conversationId);
|
||||
|
||||
if (success) {
|
||||
conversations = conversations.map((c) =>
|
||||
c.id === conversationId ? { ...c, isPinned: false } : c
|
||||
);
|
||||
// Re-sort: pinned first, then by updatedAt
|
||||
conversations = [...conversations].sort((a, b) => {
|
||||
if (a.isPinned && !b.isPinned) return -1;
|
||||
if (!a.isPinned && b.isPinned) return 1;
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
return success;
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all data
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import { chatService } from '$lib/services/chat';
|
||||
import { documentService } from '$lib/services/document';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import MessageList from '$lib/components/chat/MessageList.svelte';
|
||||
import ChatInput from '$lib/components/chat/ChatInput.svelte';
|
||||
import ChatLayout from '$lib/components/chat/ChatLayout.svelte';
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ export default defineConfig({
|
|||
'@manacore/shared-branding',
|
||||
'@manacore/shared-ui',
|
||||
'@manacore/shared-theme-ui',
|
||||
'@manacore/shared-feedback-types',
|
||||
'@manacore/shared-feedback-service',
|
||||
'@manacore/shared-feedback-ui',
|
||||
],
|
||||
},
|
||||
optimizeDeps: {
|
||||
|
|
@ -24,6 +27,9 @@ export default defineConfig({
|
|||
'@manacore/shared-branding',
|
||||
'@manacore/shared-ui',
|
||||
'@manacore/shared-theme-ui',
|
||||
'@manacore/shared-feedback-types',
|
||||
'@manacore/shared-feedback-service',
|
||||
'@manacore/shared-feedback-ui',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export interface Conversation {
|
|||
documentMode: boolean;
|
||||
title?: string;
|
||||
isArchived: boolean;
|
||||
isPinned: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue