diff --git a/apps/manacore/apps/landing/src/pages/apps/index.astro b/apps/manacore/apps/landing/src/pages/apps/index.astro index 6daaaaee2..8ced37702 100644 --- a/apps/manacore/apps/landing/src/pages/apps/index.astro +++ b/apps/manacore/apps/landing/src/pages/apps/index.astro @@ -25,7 +25,6 @@ const sections: Section[] = [ { name: 'Presi', icon: 'ph:presentation-chart-bold', tagline: 'KI Präsentationen', url: 'https://presi.mana.how' }, { name: 'Questions', icon: 'ph:magnifying-glass-bold', tagline: 'KI Research', url: 'https://questions.mana.how' }, { name: 'Context', icon: 'ph:file-text-bold', tagline: 'Dokument-Workspace', url: 'https://context.mana.how' }, - { name: 'Playground', icon: 'ph:flask-bold', tagline: 'LLM Playground', url: 'https://playground.mana.how' }, ], }, { diff --git a/apps/playground/apps/web/.dockerignore b/apps/playground/apps/web/.dockerignore deleted file mode 100644 index 15f48db44..000000000 --- a/apps/playground/apps/web/.dockerignore +++ /dev/null @@ -1,9 +0,0 @@ -node_modules -.svelte-kit -.git -*.md -.env* -!.env.example -dist -coverage -.DS_Store diff --git a/apps/playground/apps/web/.gitignore b/apps/playground/apps/web/.gitignore deleted file mode 100644 index d56da0f1e..000000000 --- a/apps/playground/apps/web/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules/ -.svelte-kit/ -.turbo/ -.env -dist/ -build/ -*.log diff --git a/apps/playground/apps/web/Dockerfile b/apps/playground/apps/web/Dockerfile deleted file mode 100644 index 0b311ce56..000000000 --- a/apps/playground/apps/web/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -# syntax=docker/dockerfile:1 -FROM sveltekit-base:local AS builder - -ARG PUBLIC_BACKEND_URL=http://mana-llm:3025 -ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-auth:3001 -ARG PUBLIC_MANA_LLM_URL=http://mana-llm:3025 -ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL -ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL -ENV PUBLIC_MANA_LLM_URL=$PUBLIC_MANA_LLM_URL - -COPY apps/playground/apps/web ./apps/playground/apps/web - -RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ - pnpm install --no-frozen-lockfile --ignore-scripts - -WORKDIR /app/apps/playground/apps/web -RUN pnpm exec svelte-kit sync -RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build - -FROM node:20-alpine AS production -WORKDIR /app/apps/playground/apps/web -COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm -COPY --from=builder /app/apps/playground/apps/web/node_modules ./node_modules -COPY --from=builder /app/apps/playground/apps/web/build ./build -COPY --from=builder /app/apps/playground/apps/web/package.json ./ - -EXPOSE 5026 -ENV NODE_ENV=production PORT=5026 HOST=0.0.0.0 - -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:5026/health || exit 1 - -CMD ["node", "build"] diff --git a/apps/playground/apps/web/package.json b/apps/playground/apps/web/package.json deleted file mode 100644 index f4cad948f..000000000 --- a/apps/playground/apps/web/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "@playground/web", - "private": true, - "version": "0.1.0", - "type": "module", - "scripts": { - "dev": "vite dev --port 5190", - "dev:full": "vite dev --port 5190", - "build": "vite build", - "preview": "vite preview", - "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" - }, - "devDependencies": { - "@sveltejs/adapter-node": "^5.4.0", - "@sveltejs/kit": "^2.47.1", - "@sveltejs/vite-plugin-svelte": "^6.2.0", - "@tailwindcss/vite": "^4.1.7", - "svelte": "^5.41.0", - "svelte-check": "^4.3.3", - "tailwindcss": "^4.1.17", - "typescript": "^5.9.3", - "vite": "^7.1.7" - }, - "dependencies": { - "@manacore/shared-auth": "workspace:*", - "@manacore/shared-auth-stores": "workspace:*", - "@manacore/shared-auth-ui": "workspace:*", - "@manacore/shared-branding": "workspace:*", - "@manacore/shared-i18n": "workspace:*", - "@manacore/shared-icons": "workspace:*", - "marked": "^17.0.0", - "svelte-i18n": "^4.0.1" - } -} diff --git a/apps/playground/apps/web/src/app.css b/apps/playground/apps/web/src/app.css deleted file mode 100644 index 0bf12813a..000000000 --- a/apps/playground/apps/web/src/app.css +++ /dev/null @@ -1,103 +0,0 @@ -@import 'tailwindcss'; - -:root { - --color-bg: #0f0f0f; - --color-surface: #1a1a1a; - --color-surface-hover: #252525; - --color-border: #333; - --color-text: #e5e5e5; - --color-text-muted: #888; - --color-primary: #3b82f6; - --color-primary-hover: #2563eb; - --color-success: #22c55e; - --color-error: #ef4444; - --color-warning: #f59e0b; -} - -html { - background-color: var(--color-bg); - color: var(--color-text); -} - -body { - font-family: - 'Inter', - -apple-system, - BlinkMacSystemFont, - 'Segoe UI', - Roboto, - sans-serif; -} - -/* Custom scrollbar */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: var(--color-surface); -} - -::-webkit-scrollbar-thumb { - background: var(--color-border); - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: #444; -} - -/* Prose styles for markdown */ -.prose { - line-height: 1.6; -} - -.prose pre { - background: var(--color-bg); - padding: 1rem; - border-radius: 0.5rem; - overflow-x: auto; - margin: 0.5rem 0; -} - -.prose code { - background: var(--color-bg); - padding: 0.125rem 0.375rem; - border-radius: 0.25rem; - font-size: 0.875em; -} - -.prose pre code { - background: transparent; - padding: 0; -} - -.prose p { - margin: 0.5rem 0; -} - -.prose ul, -.prose ol { - margin: 0.5rem 0; - padding-left: 1.5rem; -} - -.prose li { - margin: 0.25rem 0; -} - -.prose h1, -.prose h2, -.prose h3 { - margin-top: 1rem; - margin-bottom: 0.5rem; - font-weight: 600; -} - -.prose blockquote { - border-left: 3px solid var(--color-border); - padding-left: 1rem; - margin: 0.5rem 0; - color: var(--color-text-muted); -} diff --git a/apps/playground/apps/web/src/app.d.ts b/apps/playground/apps/web/src/app.d.ts deleted file mode 100644 index 3458eb1bd..000000000 --- a/apps/playground/apps/web/src/app.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/// - -declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } - - interface Window { - __PUBLIC_MANA_CORE_AUTH_URL__?: string; - __PUBLIC_MANA_LLM_URL__?: string; - } -} - -export {}; diff --git a/apps/playground/apps/web/src/app.html b/apps/playground/apps/web/src/app.html deleted file mode 100644 index 77a5ff52c..000000000 --- a/apps/playground/apps/web/src/app.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/apps/playground/apps/web/src/hooks.server.ts b/apps/playground/apps/web/src/hooks.server.ts deleted file mode 100644 index 908196b0c..000000000 --- a/apps/playground/apps/web/src/hooks.server.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Handle } from '@sveltejs/kit'; - -const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = - process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; -const PUBLIC_MANA_LLM_URL_CLIENT = - process.env.PUBLIC_MANA_LLM_URL_CLIENT || process.env.PUBLIC_MANA_LLM_URL || ''; - -export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { - transformPageChunk: ({ html }) => { - const envScript = ``; - return html.replace('', `${envScript}`); - }, - }); -}; diff --git a/apps/playground/apps/web/src/lib/api/llm.ts b/apps/playground/apps/web/src/lib/api/llm.ts deleted file mode 100644 index 5e15cb145..000000000 --- a/apps/playground/apps/web/src/lib/api/llm.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { - ChatCompletionRequest, - ChatCompletionResponse, - HealthResponse, - ModelsResponse, - StreamChunk, -} from '$lib/types'; -import { env } from '$env/dynamic/public'; -import { browser } from '$app/environment'; - -function getApiBase(): string { - if (browser) { - return ( - (window as unknown as { __PUBLIC_MANA_LLM_URL__?: string }).__PUBLIC_MANA_LLM_URL__ || - env.PUBLIC_MANA_LLM_URL || - 'http://localhost:3025' - ); - } - return env.PUBLIC_MANA_LLM_URL || 'http://localhost:3025'; -} - -export async function getHealth(): Promise { - const response = await fetch(`${getApiBase()}/health`); - if (!response.ok) { - throw new Error(`Health check failed: ${response.statusText}`); - } - return response.json(); -} - -export async function getModels(): Promise { - const response = await fetch(`${getApiBase()}/v1/models`); - if (!response.ok) { - throw new Error(`Failed to fetch models: ${response.statusText}`); - } - return response.json(); -} - -export async function sendCompletion( - request: ChatCompletionRequest -): Promise { - const response = await fetch(`${getApiBase()}/v1/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ...request, stream: false }), - }); - if (!response.ok) { - throw new Error(`Completion failed: ${response.statusText}`); - } - return response.json(); -} - -export async function* streamCompletion( - request: ChatCompletionRequest -): AsyncGenerator { - const response = await fetch(`${getApiBase()}/v1/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ...request, stream: true }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Completion failed: ${response.statusText} - ${error}`); - } - - if (!response.body) { - throw new Error('Response body is null'); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed || !trimmed.startsWith('data: ')) continue; - - const data = trimmed.slice(6); - if (data === '[DONE]') return; - - try { - const parsed: StreamChunk = JSON.parse(data); - const content = parsed.choices[0]?.delta?.content; - if (content) yield content; - } catch { - // Ignore malformed JSON chunks - } - } - } - - // Process remaining buffer - if (buffer.trim()) { - const lines = buffer.split('\n'); - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed || !trimmed.startsWith('data: ')) continue; - - const data = trimmed.slice(6); - if (data === '[DONE]') return; - - try { - const parsed: StreamChunk = JSON.parse(data); - const content = parsed.choices[0]?.delta?.content; - if (content) yield content; - } catch { - // Ignore malformed JSON chunks - } - } - } - } finally { - reader.releaseLock(); - } -} diff --git a/apps/playground/apps/web/src/lib/components/chat/ChatInput.svelte b/apps/playground/apps/web/src/lib/components/chat/ChatInput.svelte deleted file mode 100644 index 321f4d28b..000000000 --- a/apps/playground/apps/web/src/lib/components/chat/ChatInput.svelte +++ /dev/null @@ -1,80 +0,0 @@ - - -
-
- - - {#if chatStore.isStreaming} - - {:else} - - {/if} -
-
diff --git a/apps/playground/apps/web/src/lib/components/chat/ComparisonMessageBubble.svelte b/apps/playground/apps/web/src/lib/components/chat/ComparisonMessageBubble.svelte deleted file mode 100644 index a30a0c3de..000000000 --- a/apps/playground/apps/web/src/lib/components/chat/ComparisonMessageBubble.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - -
- -
-
-

{message.userContent}

-
-
- - -
-
- - Comparing {message.responses.length} models - -
-
- - -
- {#each message.responses as response (response.modelId)} - - {/each} -
-
diff --git a/apps/playground/apps/web/src/lib/components/chat/MessageBubble.svelte b/apps/playground/apps/web/src/lib/components/chat/MessageBubble.svelte deleted file mode 100644 index 03b2da0f7..000000000 --- a/apps/playground/apps/web/src/lib/components/chat/MessageBubble.svelte +++ /dev/null @@ -1,64 +0,0 @@ - - -
-
- {#if message.role === 'assistant'} -
- {#if message.isStreaming && message.content === ''} -
- - - -
- {:else} - - {@html renderedContent} - {#if message.isStreaming} - - {/if} - {/if} -
- {:else} -

{message.content}

- {/if} - -
- {formatTime(message.timestamp)} - {#if message.model} - · - {message.model.split('/').pop()} - {/if} -
-
-
diff --git a/apps/playground/apps/web/src/lib/components/chat/MessageList.svelte b/apps/playground/apps/web/src/lib/components/chat/MessageList.svelte deleted file mode 100644 index f0da2620b..000000000 --- a/apps/playground/apps/web/src/lib/components/chat/MessageList.svelte +++ /dev/null @@ -1,40 +0,0 @@ - - -
- {#if chatStore.messages.length === 0} -
-
- -
-

Start a conversation

-

- Select a model from the sidebar and send a message to begin testing the mana-llm service. -

-
- {:else} -
- {#each chatStore.messages as message (message.id)} - {#if message.role === 'comparison'} - - {:else} - - {/if} - {/each} -
- {/if} -
diff --git a/apps/playground/apps/web/src/lib/components/comparison/ComparisonResponseCard.svelte b/apps/playground/apps/web/src/lib/components/comparison/ComparisonResponseCard.svelte deleted file mode 100644 index a65ed1bd4..000000000 --- a/apps/playground/apps/web/src/lib/components/comparison/ComparisonResponseCard.svelte +++ /dev/null @@ -1,62 +0,0 @@ - - -
- -
- - {modelName} - - {#if response.isStreaming} - - Streaming... - - {:else if response.error} - Error - {:else} - Done - {/if} -
- - -
- {#if response.error} -

{response.error}

- {:else} -
{response.content}
- {#if response.isStreaming} - - {/if} - {/if} -
- - - {#if response.metrics && !response.isStreaming} -
- {formatDuration(response.metrics.durationMs)} - ~{response.metrics.completionTokens} tokens - {#if response.metrics.tokensPerSecond > 0} - {response.metrics.tokensPerSecond.toFixed(1)} t/s - {/if} -
- {/if} -
diff --git a/apps/playground/apps/web/src/lib/components/comparison/ModelComparisonSelector.svelte b/apps/playground/apps/web/src/lib/components/comparison/ModelComparisonSelector.svelte deleted file mode 100644 index 1b5e9f666..000000000 --- a/apps/playground/apps/web/src/lib/components/comparison/ModelComparisonSelector.svelte +++ /dev/null @@ -1,82 +0,0 @@ - - -
-
-

Model Comparison

- -
- - {#if comparisonStore.comparisonMode} - - -
- {#each filteredModels as model} - {@const isSelected = comparisonStore.isModelSelected(model.id)} - {@const isDisabled = !isSelected && !comparisonStore.canAddModel()} - - {/each} -
- -

- {comparisonStore.selectedModels.length}/{comparisonStore.maxModels} models selected -

- - {#if comparisonStore.selectedModels.length > 0} - - {/if} - {/if} -
diff --git a/apps/playground/apps/web/src/lib/components/comparison/ModelModalityFilter.svelte b/apps/playground/apps/web/src/lib/components/comparison/ModelModalityFilter.svelte deleted file mode 100644 index 4fcbea22c..000000000 --- a/apps/playground/apps/web/src/lib/components/comparison/ModelModalityFilter.svelte +++ /dev/null @@ -1,42 +0,0 @@ - - -
- {#each modelCounts as mod} - - {/each} -
diff --git a/apps/playground/apps/web/src/lib/components/layout/Header.svelte b/apps/playground/apps/web/src/lib/components/layout/Header.svelte deleted file mode 100644 index d1d867c0f..000000000 --- a/apps/playground/apps/web/src/lib/components/layout/Header.svelte +++ /dev/null @@ -1,82 +0,0 @@ - - -
-
- - - - - -
-

LLM Playground

-

mana-llm Service

-
-
- -
-
- {#if healthStatus === 'loading'} -
- Checking... - {:else if healthStatus === 'healthy'} -
- Healthy - {#if healthDetails} - ({healthDetails}) - {/if} - {:else} -
- Offline - {/if} -
- - -
-
diff --git a/apps/playground/apps/web/src/lib/components/layout/Sidebar.svelte b/apps/playground/apps/web/src/lib/components/layout/Sidebar.svelte deleted file mode 100644 index bc61b57bd..000000000 --- a/apps/playground/apps/web/src/lib/components/layout/Sidebar.svelte +++ /dev/null @@ -1,65 +0,0 @@ - - - diff --git a/apps/playground/apps/web/src/lib/components/settings/ModelSelector.svelte b/apps/playground/apps/web/src/lib/components/settings/ModelSelector.svelte deleted file mode 100644 index 00c6f8f99..000000000 --- a/apps/playground/apps/web/src/lib/components/settings/ModelSelector.svelte +++ /dev/null @@ -1,66 +0,0 @@ - - -
- - {#if modelsStore.loading} -
- Loading models... -
- {:else if modelsStore.error} -
-
- {modelsStore.error} -
- -
- {:else} - -
- {#if selectedModelDescription()} -

{selectedModelDescription()}

- {/if} -

{modelsStore.models.length} models available

-
- {/if} -
diff --git a/apps/playground/apps/web/src/lib/components/settings/ParameterPanel.svelte b/apps/playground/apps/web/src/lib/components/settings/ParameterPanel.svelte deleted file mode 100644 index 2935ad576..000000000 --- a/apps/playground/apps/web/src/lib/components/settings/ParameterPanel.svelte +++ /dev/null @@ -1,79 +0,0 @@ - - -
-
-
- - {settingsStore.temperature.toFixed(2)} -
- -
- Precise - Creative -
-
- -
-
- - {settingsStore.maxTokens} -
- -
- 256 - 8192 -
-
- -
-
- - {settingsStore.topP.toFixed(2)} -
- -
- Focused - Diverse -
-
-
diff --git a/apps/playground/apps/web/src/lib/components/settings/SystemPromptEditor.svelte b/apps/playground/apps/web/src/lib/components/settings/SystemPromptEditor.svelte deleted file mode 100644 index ad8d93ce5..000000000 --- a/apps/playground/apps/web/src/lib/components/settings/SystemPromptEditor.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - -
-
- - -
- -

- {settingsStore.systemPrompt.length} characters -

-
diff --git a/apps/playground/apps/web/src/lib/stores/auth.svelte.ts b/apps/playground/apps/web/src/lib/stores/auth.svelte.ts deleted file mode 100644 index 5df932791..000000000 --- a/apps/playground/apps/web/src/lib/stores/auth.svelte.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Auth Store — uses centralized Mana auth factory. - */ - -import { createManaAuthStore } from '@manacore/shared-auth-stores'; - -export const authStore = createManaAuthStore(); diff --git a/apps/playground/apps/web/src/lib/stores/chat.svelte.ts b/apps/playground/apps/web/src/lib/stores/chat.svelte.ts deleted file mode 100644 index 2c00b69e9..000000000 --- a/apps/playground/apps/web/src/lib/stores/chat.svelte.ts +++ /dev/null @@ -1,205 +0,0 @@ -import type { AnyMessage, ChatMessage, ComparisonMessage, Message } from '$lib/types'; -import { streamCompletion } from '$lib/api/llm'; -import { settingsStore } from './settings.svelte'; -import { comparisonStore } from './comparison.svelte'; - -function generateId(): string { - return crypto.randomUUID(); -} - -function createChatStore() { - let messages = $state([]); - let isStreaming = $state(false); - let abortController = $state(null); - - // Helper to extract conversation history for API calls - function getConversationHistory(): Message[] { - const history: Message[] = []; - for (const msg of messages) { - if (msg.role === 'user') { - history.push({ role: 'user', content: (msg as ChatMessage).content }); - } else if (msg.role === 'assistant' && !(msg as ChatMessage).isStreaming) { - history.push({ role: 'assistant', content: (msg as ChatMessage).content }); - } - // Skip comparison messages in history for now - } - return history; - } - - return { - get messages() { - return messages; - }, - get isStreaming() { - return isStreaming; - }, - - async sendMessage(content: string) { - if (isStreaming || !content.trim()) return; - - // Add user message - const userMessage: ChatMessage = { - id: generateId(), - role: 'user', - content: content.trim(), - timestamp: new Date(), - }; - messages.push(userMessage); - - // Create assistant message placeholder - const assistantMessage: ChatMessage = { - id: generateId(), - role: 'assistant', - content: '', - timestamp: new Date(), - model: settingsStore.model, - isStreaming: true, - }; - messages.push(assistantMessage); - - // Build messages for API - const apiMessages: Message[] = []; - - if (settingsStore.systemPrompt.trim()) { - apiMessages.push({ - role: 'system', - content: settingsStore.systemPrompt, - }); - } - - for (const msg of messages) { - if (msg.role === 'comparison') continue; // Skip comparison messages - const chatMsg = msg as ChatMessage; - if (chatMsg.role === 'user' || (chatMsg.role === 'assistant' && !chatMsg.isStreaming)) { - apiMessages.push({ - role: chatMsg.role, - content: chatMsg.content, - }); - } - } - - // Start streaming - isStreaming = true; - abortController = new AbortController(); - - try { - const stream = streamCompletion({ - model: settingsStore.model, - messages: apiMessages, - temperature: settingsStore.temperature, - max_tokens: settingsStore.maxTokens, - top_p: settingsStore.topP, - stream: true, - }); - - for await (const chunk of stream) { - // Find and update the assistant message - const idx = messages.findIndex((m) => m.id === assistantMessage.id); - if (idx !== -1) { - (messages[idx] as ChatMessage).content += chunk; - } - } - - // Mark streaming complete - const idx = messages.findIndex((m) => m.id === assistantMessage.id); - if (idx !== -1) { - (messages[idx] as ChatMessage).isStreaming = false; - (messages[idx] as ChatMessage).timestamp = new Date(); - } - } catch (error) { - // Update message with error - const idx = messages.findIndex((m) => m.id === assistantMessage.id); - if (idx !== -1) { - (messages[idx] as ChatMessage).content = - `Error: ${error instanceof Error ? error.message : 'Unknown error'}`; - (messages[idx] as ChatMessage).isStreaming = false; - } - } finally { - isStreaming = false; - abortController = null; - } - }, - - async sendComparisonMessage(content: string) { - if (isStreaming || !content.trim()) return; - if (comparisonStore.selectedModels.length < 2) return; - - const comparisonMsg: ComparisonMessage = { - id: generateId(), - role: 'comparison', - userContent: content.trim(), - responses: [], - timestamp: new Date(), - }; - - messages = [...messages, comparisonMsg]; - isStreaming = true; - - try { - const history = getConversationHistory(); - await comparisonStore.compareModels( - content.trim(), - comparisonStore.selectedModels, - history, - (responses) => { - const idx = messages.findIndex((m) => m.id === comparisonMsg.id); - if (idx !== -1) { - (messages[idx] as ComparisonMessage).responses = responses; - messages = [...messages]; - } - } - ); - } finally { - isStreaming = false; - } - }, - - stopStreaming() { - if (abortController) { - abortController.abort(); - } - }, - - clearMessages() { - messages = []; - isStreaming = false; - }, - - exportMessages(): string { - return JSON.stringify( - { - exported: new Date().toISOString(), - settings: { - model: settingsStore.model, - temperature: settingsStore.temperature, - maxTokens: settingsStore.maxTokens, - topP: settingsStore.topP, - systemPrompt: settingsStore.systemPrompt, - }, - messages: messages.map((m) => { - if (m.role === 'comparison') { - const compMsg = m as ComparisonMessage; - return { - role: 'comparison', - userContent: compMsg.userContent, - responses: compMsg.responses, - timestamp: compMsg.timestamp, - }; - } - const chatMsg = m as ChatMessage; - return { - role: chatMsg.role, - content: chatMsg.content, - timestamp: chatMsg.timestamp, - model: chatMsg.model, - }; - }), - }, - null, - 2 - ); - }, - }; -} - -export const chatStore = createChatStore(); diff --git a/apps/playground/apps/web/src/lib/stores/comparison.svelte.ts b/apps/playground/apps/web/src/lib/stores/comparison.svelte.ts deleted file mode 100644 index caadb6b45..000000000 --- a/apps/playground/apps/web/src/lib/stores/comparison.svelte.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { streamCompletion } from '$lib/api/llm'; -import type { ComparisonResponse, ChatCompletionRequest, Message } from '$lib/types'; -import { settingsStore } from './settings.svelte'; - -function createComparisonStore() { - let comparisonMode = $state(false); - let selectedModels = $state([]); - const maxModels = 4; - - return { - get comparisonMode() { - return comparisonMode; - }, - get selectedModels() { - return selectedModels; - }, - get maxModels() { - return maxModels; - }, - - toggleComparisonMode() { - comparisonMode = !comparisonMode; - if (!comparisonMode) { - selectedModels = []; - } - }, - - setComparisonMode(value: boolean) { - comparisonMode = value; - if (!value) { - selectedModels = []; - } - }, - - toggleModel(modelId: string) { - if (selectedModels.includes(modelId)) { - selectedModels = selectedModels.filter((m) => m !== modelId); - } else if (selectedModels.length < maxModels) { - selectedModels = [...selectedModels, modelId]; - } - }, - - isModelSelected(modelId: string): boolean { - return selectedModels.includes(modelId); - }, - - clearSelection() { - selectedModels = []; - }, - - canAddModel(): boolean { - return selectedModels.length < maxModels; - }, - - async compareModels( - content: string, - models: string[], - conversationHistory: Message[], - onUpdate: (responses: ComparisonResponse[]) => void - ): Promise { - const responses: ComparisonResponse[] = models.map((modelId) => ({ - modelId, - content: '', - isStreaming: true, - startTime: Date.now(), - })); - - onUpdate([...responses]); - - // Build base messages including history - const baseMessages: Message[] = []; - - if (settingsStore.systemPrompt.trim()) { - baseMessages.push({ - role: 'system', - content: settingsStore.systemPrompt, - }); - } - - // Add conversation history - baseMessages.push(...conversationHistory); - - // Add current user message - baseMessages.push({ - role: 'user', - content, - }); - - // Start parallel streams for all models - const streamPromises = models.map(async (modelId, index) => { - const request: ChatCompletionRequest = { - model: modelId, - messages: baseMessages, - temperature: settingsStore.temperature, - max_tokens: settingsStore.maxTokens, - top_p: settingsStore.topP, - stream: true, - }; - - try { - let tokenCount = 0; - for await (const chunk of streamCompletion(request)) { - responses[index].content += chunk; - tokenCount++; - onUpdate([...responses]); - } - - responses[index].isStreaming = false; - responses[index].endTime = Date.now(); - - const durationMs = responses[index].endTime! - responses[index].startTime; - // Estimate tokens (rough approximation based on whitespace-split words) - const estimatedTokens = responses[index].content.split(/\s+/).length; - - responses[index].metrics = { - promptTokens: 0, // Not available from stream - completionTokens: estimatedTokens, - totalTokens: estimatedTokens, - durationMs, - tokensPerSecond: durationMs > 0 ? (estimatedTokens / durationMs) * 1000 : 0, - }; - } catch (error) { - responses[index].isStreaming = false; - responses[index].error = error instanceof Error ? error.message : 'Unknown error'; - responses[index].endTime = Date.now(); - } - - onUpdate([...responses]); - }); - - await Promise.all(streamPromises); - return responses; - }, - }; -} - -export const comparisonStore = createComparisonStore(); diff --git a/apps/playground/apps/web/src/lib/stores/models.svelte.ts b/apps/playground/apps/web/src/lib/stores/models.svelte.ts deleted file mode 100644 index b252c28c0..000000000 --- a/apps/playground/apps/web/src/lib/stores/models.svelte.ts +++ /dev/null @@ -1,196 +0,0 @@ -import type { Model, ModelWithModality, Modality, Provider } from '$lib/types'; -import { getModels } from '$lib/api/llm'; - -/** - * Model metadata configuration - * Add new models here when installing them on the server - * See: docs/OLLAMA_MODELS.md for instructions - */ -export const MODEL_METADATA: Record = { - // Text Models - General Purpose - 'gemma3:4b': { - description: 'Fast general-purpose model (~53 t/s)', - modality: 'text', - }, - 'gemma3:12b': { - description: 'Balanced quality & speed (~30 t/s)', - modality: 'text', - }, - 'gemma3:27b': { - description: 'Best quality, slower (~15 t/s)', - modality: 'text', - }, - 'phi3.5:latest': { - description: 'Microsoft Phi-3.5 - compact & efficient', - modality: 'text', - }, - 'ministral-3:3b': { - description: 'Mistral Mini - fast for simple tasks', - modality: 'text', - }, - - // Vision Models - 'llava:7b': { - description: 'Image understanding & description', - modality: 'vision', - }, - 'qwen3-vl:4b': { - description: 'Qwen Vision-Language model', - modality: 'vision', - }, - 'deepseek-ocr:latest': { - description: 'OCR & document understanding', - modality: 'vision', - }, - - // Code Models - 'qwen2.5-coder:7b': { - description: 'Code generation & completion (7B)', - modality: 'code', - }, - 'qwen2.5-coder:14b': { - description: 'Advanced code generation (14B)', - modality: 'code', - }, -}; - -/** - * Detect modality from model ID - * First checks MODEL_METADATA, then falls back to pattern matching - */ -function detectModality(modelId: string): Modality { - const id = modelId.toLowerCase(); - - // Extract model name from provider prefix (e.g., "ollama/gemma3:4b" -> "gemma3:4b") - const modelName = id.includes('/') ? id.split('/').slice(1).join('/') : id; - - // Check metadata first - if (MODEL_METADATA[modelName]) { - return MODEL_METADATA[modelName].modality; - } - - // Vision models (pattern matching fallback) - if ( - id.includes('llava') || - id.includes('vision') || - id.includes('-vl') || - id.includes('ocr') || - id.includes('moondream') - ) { - return 'vision'; - } - - // Code models (pattern matching fallback) - if (id.includes('coder') || id.includes('codellama') || id.includes('starcoder')) { - return 'code'; - } - - // Default to text - return 'text'; -} - -/** - * Get model description from metadata - */ -function getModelDescription(modelId: string): string | undefined { - const modelName = modelId.includes('/') ? modelId.split('/').slice(1).join('/') : modelId; - return MODEL_METADATA[modelName]?.description; -} - -interface GroupedModels { - provider: Provider; - label: string; - models: Model[]; -} - -function createModelsStore() { - let models = $state([]); - let loading = $state(false); - let error = $state(null); - - // Models with modality information for comparison - const modelsWithModality = $derived( - models.map((model) => ({ - ...model, - modality: detectModality(model.id), - description: getModelDescription(model.id), - })) - ); - - const groupedModels = $derived.by(() => { - const groups: Record = { - ollama: [], - openrouter: [], - groq: [], - together: [], - }; - - for (const model of models) { - const id = model.id; - if (id.startsWith('ollama/')) { - groups.ollama.push(model); - } else if (id.startsWith('openrouter/')) { - groups.openrouter.push(model); - } else if (id.startsWith('groq/')) { - groups.groq.push(model); - } else if (id.startsWith('together/')) { - groups.together.push(model); - } - } - - const result: GroupedModels[] = []; - const labels: Record = { - ollama: 'Ollama (Local)', - openrouter: 'OpenRouter', - groq: 'Groq', - together: 'Together AI', - }; - - for (const [provider, providerModels] of Object.entries(groups)) { - if (providerModels.length > 0) { - result.push({ - provider: provider as Provider, - label: labels[provider as Provider], - models: providerModels.sort((a, b) => a.id.localeCompare(b.id)), - }); - } - } - - return result; - }); - - return { - get models() { - return models; - }, - get loading() { - return loading; - }, - get error() { - return error; - }, - get groupedModels() { - return groupedModels; - }, - get modelsWithModality() { - return modelsWithModality; - }, - - async loadModels() { - loading = true; - error = null; - - try { - const response = await getModels(); - models = response.data; - } catch (e) { - error = e instanceof Error ? e.message : 'Failed to load models'; - models = []; - } finally { - loading = false; - } - }, - }; -} - -export const modelsStore = createModelsStore(); diff --git a/apps/playground/apps/web/src/lib/stores/settings.svelte.ts b/apps/playground/apps/web/src/lib/stores/settings.svelte.ts deleted file mode 100644 index 8d02af78e..000000000 --- a/apps/playground/apps/web/src/lib/stores/settings.svelte.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { Settings } from '$lib/types'; -import { browser } from '$app/environment'; - -const STORAGE_KEY = 'llm-playground-settings'; - -const defaultSettings: Settings = { - model: 'ollama/gemma3:4b', - temperature: 0.7, - maxTokens: 2048, - topP: 1.0, - systemPrompt: 'You are a helpful AI assistant.', -}; - -function loadSettings(): Settings { - if (!browser) return defaultSettings; - - try { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - return { ...defaultSettings, ...JSON.parse(stored) }; - } - } catch { - // Ignore parse errors - } - return defaultSettings; -} - -function saveSettings(settings: Settings): void { - if (!browser) return; - localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); -} - -function createSettingsStore() { - let settings = $state(loadSettings()); - - return { - get model() { - return settings.model; - }, - set model(value: string) { - settings.model = value; - saveSettings(settings); - }, - - get temperature() { - return settings.temperature; - }, - set temperature(value: number) { - settings.temperature = value; - saveSettings(settings); - }, - - get maxTokens() { - return settings.maxTokens; - }, - set maxTokens(value: number) { - settings.maxTokens = value; - saveSettings(settings); - }, - - get topP() { - return settings.topP; - }, - set topP(value: number) { - settings.topP = value; - saveSettings(settings); - }, - - get systemPrompt() { - return settings.systemPrompt; - }, - set systemPrompt(value: string) { - settings.systemPrompt = value; - saveSettings(settings); - }, - - reset() { - settings = { ...defaultSettings }; - saveSettings(settings); - }, - }; -} - -export const settingsStore = createSettingsStore(); diff --git a/apps/playground/apps/web/src/lib/types/index.ts b/apps/playground/apps/web/src/lib/types/index.ts deleted file mode 100644 index a0905f76c..000000000 --- a/apps/playground/apps/web/src/lib/types/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -export interface Message { - role: 'system' | 'user' | 'assistant'; - content: string; -} - -export interface ChatCompletionRequest { - model: string; - messages: Message[]; - temperature?: number; - max_tokens?: number; - top_p?: number; - stream?: boolean; -} - -export interface ChatCompletionChoice { - index: number; - message: Message; - finish_reason: string | null; -} - -export interface ChatCompletionResponse { - id: string; - object: string; - created: number; - model: string; - choices: ChatCompletionChoice[]; - usage?: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; -} - -export interface StreamDelta { - role?: string; - content?: string; -} - -export interface StreamChoice { - index: number; - delta: StreamDelta; - finish_reason: string | null; -} - -export interface StreamChunk { - id: string; - object: string; - created: number; - model: string; - choices: StreamChoice[]; -} - -export interface Model { - id: string; - object: string; - created: number; - owned_by: string; -} - -export interface ModelsResponse { - object: string; - data: Model[]; -} - -export interface HealthResponse { - status: string; - timestamp: string; - version?: string; -} - -export interface ChatMessage { - id: string; - role: 'user' | 'assistant'; - content: string; - timestamp: Date; - model?: string; - isStreaming?: boolean; -} - -export interface Settings { - model: string; - temperature: number; - maxTokens: number; - topP: number; - systemPrompt: string; -} - -export type Provider = 'ollama' | 'openrouter' | 'groq' | 'together'; - -// Modality types for model comparison -export type Modality = 'text' | 'vision' | 'code'; - -export interface ModelWithModality extends Model { - modality: Modality; - description?: string; -} - -// Comparison response from a single model -export interface ComparisonResponse { - modelId: string; - content: string; - isStreaming: boolean; - startTime: number; - endTime?: number; - metrics?: { - promptTokens: number; - completionTokens: number; - totalTokens: number; - durationMs: number; - tokensPerSecond: number; - }; - error?: string; -} - -// Comparison message containing multiple model responses -export interface ComparisonMessage { - id: string; - role: 'comparison'; - userContent: string; - responses: ComparisonResponse[]; - timestamp: Date; -} - -// Union type for all message types -export type AnyMessage = ChatMessage | ComparisonMessage; diff --git a/apps/playground/apps/web/src/routes/(auth)/+layout.svelte b/apps/playground/apps/web/src/routes/(auth)/+layout.svelte deleted file mode 100644 index fd5e639d7..000000000 --- a/apps/playground/apps/web/src/routes/(auth)/+layout.svelte +++ /dev/null @@ -1,6 +0,0 @@ - - -{@render children()} diff --git a/apps/playground/apps/web/src/routes/(auth)/login/+page.svelte b/apps/playground/apps/web/src/routes/(auth)/login/+page.svelte deleted file mode 100644 index 1dbd5f445..000000000 --- a/apps/playground/apps/web/src/routes/(auth)/login/+page.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - - - {translations.title} | LLM Playground - - - authStore.signInWithPasskey()} - onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)} - onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)} - onSendMagicLink={(email) => authStore.sendMagicLink(email)} - {goto} - successRedirect={redirectTo} - registerPath="/register" - forgotPasswordPath="/forgot-password" - lightBackground="#ecfeff" - darkBackground="#083344" - {translations} - {verified} - {initialEmail} -/> diff --git a/apps/playground/apps/web/src/routes/(auth)/register/+page.svelte b/apps/playground/apps/web/src/routes/(auth)/register/+page.svelte deleted file mode 100644 index 4cfec7ce2..000000000 --- a/apps/playground/apps/web/src/routes/(auth)/register/+page.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - - - {translations.title} | LLM Playground - - - diff --git a/apps/playground/apps/web/src/routes/(protected)/+layout.svelte b/apps/playground/apps/web/src/routes/(protected)/+layout.svelte deleted file mode 100644 index d98d62268..000000000 --- a/apps/playground/apps/web/src/routes/(protected)/+layout.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -{#if isChecking} -
-
-
-{:else} - {@render children()} -{/if} diff --git a/apps/playground/apps/web/src/routes/(protected)/+page.svelte b/apps/playground/apps/web/src/routes/(protected)/+page.svelte deleted file mode 100644 index fc758c74f..000000000 --- a/apps/playground/apps/web/src/routes/(protected)/+page.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - LLM Playground | mana-llm - - -
- -
- - -
- - -
-
diff --git a/apps/playground/apps/web/src/routes/+layout.svelte b/apps/playground/apps/web/src/routes/+layout.svelte deleted file mode 100644 index 0b8a3f4c7..000000000 --- a/apps/playground/apps/web/src/routes/+layout.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -
- {@render children()} -
diff --git a/apps/playground/apps/web/src/routes/health/+server.ts b/apps/playground/apps/web/src/routes/health/+server.ts deleted file mode 100644 index 991cbfb8f..000000000 --- a/apps/playground/apps/web/src/routes/health/+server.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; - -export const GET: RequestHandler = async () => { - return json({ - status: 'healthy', - service: 'llm-playground', - timestamp: new Date().toISOString(), - }); -}; diff --git a/apps/playground/apps/web/static/favicon.png b/apps/playground/apps/web/static/favicon.png deleted file mode 100644 index d831d66d7..000000000 Binary files a/apps/playground/apps/web/static/favicon.png and /dev/null differ diff --git a/apps/playground/apps/web/svelte.config.js b/apps/playground/apps/web/svelte.config.js deleted file mode 100644 index fc92816a8..000000000 --- a/apps/playground/apps/web/svelte.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import adapter from '@sveltejs/adapter-node'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - preprocess: vitePreprocess(), - kit: { - adapter: adapter(), - }, -}; - -export default config; diff --git a/apps/playground/apps/web/tsconfig.json b/apps/playground/apps/web/tsconfig.json deleted file mode 100644 index a8f10c8e3..000000000 --- a/apps/playground/apps/web/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } -} diff --git a/apps/playground/apps/web/vite.config.ts b/apps/playground/apps/web/vite.config.ts deleted file mode 100644 index 138c229a6..000000000 --- a/apps/playground/apps/web/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import tailwindcss from '@tailwindcss/vite'; -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [tailwindcss(), sveltekit()], -}); diff --git a/apps/playground/package.json b/apps/playground/package.json deleted file mode 100644 index 9a4a81652..000000000 --- a/apps/playground/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "playground", - "private": true, - "scripts": { - "dev": "turbo run dev" - } -} diff --git a/apps/reader/.gitignore b/apps/reader/.gitignore deleted file mode 100644 index f5f1c697b..000000000 --- a/apps/reader/.gitignore +++ /dev/null @@ -1,40 +0,0 @@ -# Dependencies -node_modules/ - -# Build outputs -dist/ -web-build/ - -# Expo -.expo/ -expo-env.d.ts - -# React Native (generated via expo prebuild) -ios/ -android/ - -# Environment -.env -.env.local -.env.production - -# Debug -npm-debug.* -*.log - -# Certificates & Keys -*.jks -*.p8 -*.p12 -*.key -*.mobileprovision -*.orig.* - -# Metro -.metro-health-check* - -# IDE -.idea/ - -# OS -.DS_Store diff --git a/apps/reader/CLAUDE.md b/apps/reader/CLAUDE.md deleted file mode 100644 index c2e841e39..000000000 --- a/apps/reader/CLAUDE.md +++ /dev/null @@ -1,149 +0,0 @@ -# CLAUDE.md - Reader - -This file provides guidance to Claude Code when working with the Reader project. - -## Project Overview - -Reader is a Text-to-Speech React Native application built with Expo that converts text to high-quality audio using Google Chirp voices. It stores audio locally for offline playback and syncs data across devices via Supabase. - -## Architecture - -``` -apps/reader/ -├── apps/ -│ └── mobile/ # Expo React Native App (@reader/mobile) -│ ├── app/ # Expo Router navigation -│ │ ├── (tabs)/ # Tab navigation screens -│ │ ├── (auth)/ # Auth flow routes -│ │ └── _layout.tsx -│ ├── components/ # Reusable UI components -│ ├── hooks/ # Custom React hooks -│ ├── services/ # Business logic services -│ ├── store/ # Zustand state management -│ ├── types/ # TypeScript types -│ ├── utils/ # Utilities (Supabase client, etc.) -│ ├── assets/ # Images, fonts -│ └── package.json # @reader/mobile -├── packages/ # For future shared code -├── CLAUDE.md # This file -└── .gitignore -``` - -## Development Commands - -```bash -# From monorepo root -pnpm install - -# Start Reader mobile app -pnpm reader:dev -# Or directly -pnpm dev:reader:mobile - -# From apps/reader/apps/mobile/ -pnpm dev # Start Expo dev server -pnpm ios # Run on iOS simulator -pnpm android # Run on Android emulator -pnpm web # Run on web - -# Code quality -pnpm lint # Run ESLint -pnpm format # Format with Prettier - -# Build for production -pnpm build:preview # Preview build -pnpm build:prod # Production build -``` - -## Tech Stack - -| Component | Technology | -| ---------- | --------------------------------- | -| Framework | React Native 0.79.5 + Expo SDK 53 | -| Navigation | Expo Router v5 (file-based) | -| Styling | NativeWind (Tailwind CSS for RN) | -| State | Zustand | -| Backend | Supabase (PostgreSQL + Auth) | -| Language | TypeScript | - -## Database Design - -Single `texts` table with JSONB field for flexibility: - -- Stores texts, metadata, tags, and reading progress -- Audio files stored locally, paths tracked in DB -- Designed for future expansion without migrations - -See `apps/mobile/ReadMe/MinimalDatabase.md` for details. - -## Key Implementation Patterns - -### Navigation (Expo Router) - -```tsx -// File-based routing in apps/mobile/app/ -// (tabs)/ - Tab navigation screens -// (auth)/ - Auth flow routes -``` - -### Styling (NativeWind) - -```tsx - - Hello - -``` - -### State Management (Zustand) - -```tsx -import { useStore } from '~/store/store'; -const { state, actions } = useStore(); -``` - -### Supabase Client - -```tsx -// Client configured in apps/mobile/utils/supabase.ts -import { supabase } from '~/utils/supabase'; -``` - -### Path Alias - -Use `~/*` for absolute imports from mobile root: - -```tsx -import { Button } from '~/components/Button'; -``` - -## Environment Variables - -Create `apps/reader/apps/mobile/.env`: - -```bash -EXPO_PUBLIC_SUPABASE_URL=your_supabase_url -EXPO_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key -``` - -## Current Implementation Status - -- [x] Expo Router setup with tab navigation -- [x] Supabase integration -- [x] Zustand store (user state, settings, audio player) -- [x] NativeWind styling -- [x] User authentication (Login, Register, Forgot Password) -- [x] Text management UI (List, Add, View, Delete) -- [x] Settings screen -- [x] Text-to-Speech with Google Cloud TTS -- [x] Audio player with progress tracking -- [x] Offline audio storage (Expo FileSystem) -- [x] Tag system with filtering -- [x] Supabase Edge Functions for audio generation -- [x] Audio chunk system for large texts -- [x] Local audio caching - -## Detailed Documentation - -- `apps/mobile/ReadMe/ProjectOverview.md` - Project vision (German) -- `apps/mobile/ReadMe/MinimalDatabase.md` - Database design -- `apps/mobile/docs/` - Additional documentation diff --git a/apps/reader/apps/mobile/CONTEXT_MENU_SOLUTION.md b/apps/reader/apps/mobile/CONTEXT_MENU_SOLUTION.md deleted file mode 100644 index df05d5831..000000000 --- a/apps/reader/apps/mobile/CONTEXT_MENU_SOLUTION.md +++ /dev/null @@ -1,64 +0,0 @@ -# Context Menu Solution - -I've fixed the "View config not found for component 'ContextMenu'" error by replacing the native `react-native-context-menu-view` with a cross-platform solution that works with Expo Go. - -## What was changed: - -1. **Removed the native dependency**: Uninstalled `react-native-context-menu-view` which requires native code and doesn't work with Expo Go. - -2. **Created two alternative solutions**: - -### Option 1: ActionMenu (Currently Active) - -- Located in `/components/ActionMenu.tsx` -- Uses native ActionSheetIOS on iOS for a truly native experience -- Custom modal implementation for Android that slides up from bottom -- Triggered by long press on list items - -### Option 2: ContextMenu (Alternative) - -- Located in `/components/ContextMenu.tsx` -- Custom modal-based context menu that appears near the pressed item -- Works on both platforms with consistent behavior -- More traditional context menu appearance - -## How it works: - -The ActionMenu component wraps your list items and provides a long-press gesture handler. When activated: - -- On iOS: Shows native ActionSheetIOS -- On Android: Shows a custom bottom sheet modal - -## Usage: - -```tsx - { - // Handle selection - }} -> - - -``` - -## Benefits: - -- Works with Expo Go (no development build required) -- Native feel on iOS -- Consistent experience across platforms -- No native dependencies - -## If you need a development build: - -If you prefer to use the native context menu and are willing to use a development build: - -1. Re-install `react-native-context-menu-view` -2. Run `npx expo prebuild` -3. Run `npx expo run:ios` or `npx expo run:android` - -The current solution allows you to continue using Expo Go for development while providing a good user experience. diff --git a/apps/reader/apps/mobile/ReadMe/AudioPlayerImprovements.md b/apps/reader/apps/mobile/ReadMe/AudioPlayerImprovements.md deleted file mode 100644 index 9593d4315..000000000 --- a/apps/reader/apps/mobile/ReadMe/AudioPlayerImprovements.md +++ /dev/null @@ -1,78 +0,0 @@ -⏺ AudioPlayer Verbesserungsplan - -1. Geschwindigkeitssteuerung - -- Playback-Speed Kontrolle - (0.5x, 0.75x, 1x, 1.25x, 1.5x, - 2x) -- Geschwindigkeit bleibt für - alle Audios gespeichert -- Kleiner Button neben den - Controls - -2. Visuelle Verbesserungen - -- Waveform-Visualisierung - während der Wiedergabe -- Bessere Progress-Bar mit - Touch-Gesten zum Scrubben -- Pulsierender Play-Button - während des Ladens - -3. Erweiterte Navigation - -- Kapitel-Support (bei - längeren Texten) -- Lesezeichen setzen während - der Wiedergabe -- Zu bestimmten Zeitstempeln - springen - -4. Sleep Timer - -- Timer zum automatischen - Stoppen (15, 30, 45, 60 Min) -- Fade-Out am Ende -- Visual Countdown - -5. Kontinuierliche Wiedergabe - -- Automatisch nächsten Text - abspielen -- Queue-System für mehrere - Texte -- Shuffle-Modus - -6. Mini-Player - -- Kompakter Player am unteren - Bildschirmrand -- Bleibt beim Navigieren - sichtbar -- Swipe-to-dismiss - -7. Offline-Optimierung - -- Download-Button für lokale - Speicherung -- Download-Progress anzeigen -- Cache-Management UI - -8. Statistiken & History - -- Listening History anzeigen -- Fortschritt pro Text tracken -- Gesamte Hörzeit - -9. Accessibility - -- VoiceOver Support verbessern -- Größere Touch-Targets -- Keyboard-Shortcuts (iPad) - -10. Performance - -- Preloading des nächsten - Chunks -- Smooth Chunk-Übergänge -- Background Audio optimieren diff --git a/apps/reader/apps/mobile/ReadMe/ExpoUI.md b/apps/reader/apps/mobile/ReadMe/ExpoUI.md deleted file mode 100644 index 8d551bba9..000000000 --- a/apps/reader/apps/mobile/ReadMe/ExpoUI.md +++ /dev/null @@ -1,226 +0,0 @@ -Expo UI - -A set of components that allow you to build UIs directly with SwiftUI and Jetpack Compose from React. - -Bundled version: -~0.1.1-alpha.10 -This library is currently in alpha and will frequently experience breaking changes. It is not available in the Expo Go app – use development builds to try it out. -@expo/ui is a set of native input components that allows you to build fully native interfaces with SwiftUI and Jetpack Compose. It aims to provide the commonly used features and components that a typical app will need. - -Installation -Terminal - -Copy - -npx expo install @expo/ui -If you are installing this in an existing React Native app, make sure to install expo in your project. - -Swift UI examples -BottomSheet - -iOS - -Code - -BottomSheet component on iOS. -Button - -iOS - -Code - -Button component on iOS. -CircularProgress - -iOS - -Code - -CircularProgress component on iOS. -ColorPicker - -iOS - -Code - -ColorPicker component on iOS. -ContextMenu -Note: Also known as DropdownMenu. - -iOS - -Code - -ContextMenu component on iOS. -DateTimePicker (date) - -iOS - -Code - -DateTimePicker (date) component on iOS. -DateTimePicker (time) - -iOS - -Code - -DateTimePicker (time) component on iOS. -Gauge - -iOS - -Code - -Gauge component on iOS. -LinearProgress - -iOS - -Code - -LinearProgress component on iOS. -List - -iOS - -Code - -List component on iOS. -Picker (segmented) - -iOS - -Code - -Picker component on iOS. -Picker (wheel) - -iOS - -Code - -Picker component on iOS. -Slider - -iOS - -Code - -Slider component on iOS. -Switch (toggle) -Note: Also known as Toggle. - -iOS - -Code - -Switch component on iOS. -Switch (checkbox) - -iOS - -Code - -Picker component on iOS. -TextInput - -iOS - -Code - -TextInput component on iOS. -Jetpack Compose examples -Button - -Android - -Code - -Button component on Android. -CircularProgress - -Android - -Code - -CircularProgress component on Android. -ContextMenu -Note: Also known as DropdownMenu. - -Android - -Code - -ContextMenu component on Android. -DateTimePicker (date) - -Android - -Code - -DateTimePicker component on Android. -DateTimePicker (time) - -Android - -Code - -DateTimePicker (time) component on Android. -LinearProgress - -Android - -Code - -LinearProgress component on Android. -Picker (radio) - -Android - -Code - -Picker component (radio) on Android. -Picker (segmented) - -Android - -Code - -Picker component on Android. -Slider - -Android - -Code - -Slider component on Android. -Switch (toggle) -Note: Also known as Toggle. - -Android - -Code - -Switch component on Android. -Switch (checkbox) - -Android - -Code - -Switch (checkbox variant) component on Android. -TextInput - -Android - -Code - -TextInput component on Android. -API -Full documentation is not yet available. Use TypeScript types to explore the API. - -// Import from the SwiftUI package -import { BottomSheet } from '@expo/ui/swift-ui'; -// Import from the Jetpack Compose package -import { Button } from '@expo/ui/jetpack-compose'; diff --git a/apps/reader/apps/mobile/ReadMe/MinimalDatabase.md b/apps/reader/apps/mobile/ReadMe/MinimalDatabase.md deleted file mode 100644 index f2f351a85..000000000 --- a/apps/reader/apps/mobile/ReadMe/MinimalDatabase.md +++ /dev/null @@ -1,562 +0,0 @@ -# Absolut Minimalste Text-to-Speech Datenbank - -## Philosophie - -Eine einzige Tabelle für alles. JSONB macht's möglich. Keine Joins, keine Komplexität, nur pure Funktionalität. - -## Die Eine Tabelle - -```sql --- Die einzige Tabelle die du brauchst -CREATE TABLE texts ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, - - -- Der eigentliche Content - title TEXT NOT NULL, - content TEXT NOT NULL, - - -- ALLES andere in einem JSONB Feld - data JSONB DEFAULT '{}' NOT NULL, - - -- Nur die absolut nötigen Timestamps - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- Ein Index für Performance -CREATE INDEX idx_texts_user ON texts(user_id); -CREATE INDEX idx_texts_data ON texts USING GIN (data); - --- RLS aktivieren -ALTER TABLE texts ENABLE ROW LEVEL SECURITY; - --- Jeder sieht nur seine eigenen Texte -CREATE POLICY "Own texts only" ON texts - FOR ALL USING (auth.uid() = user_id); - --- Update Timestamp Trigger -CREATE OR REPLACE FUNCTION update_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER update_texts_updated_at - BEFORE UPDATE ON texts - FOR EACH ROW - EXECUTE FUNCTION update_updated_at(); -``` - -## Was kommt ins `data` JSONB Feld? - -```javascript -// Beispiel eines vollständigen Text-Objekts -{ - id: "uuid-hier", - user_id: "user-uuid", - title: "Mein Buch", - content: "Der eigentliche Text...", - data: { - // Vorlese-Einstellungen - tts: { - speed: 1.0, - voice: "de-DE", - lastPosition: 1234, // Zeichen-Position - lastPlayed: "2024-01-15T10:30:00Z" - }, - - // Audio-Cache (NEU!) - audio: { - hasLocalCache: false, - chunks: [ - { - id: "chunk-1", - start: 0, - end: 1000, // Zeichen-Position - filename: "text-uuid-chunk-1.mp3", - size: 245760, // Bytes - duration: 120, // Sekunden - createdAt: "2024-01-15T10:00:00Z" - } - ], - totalSize: 2457600, // Total in Bytes - lastGenerated: "2024-01-15T10:00:00Z", - settings: { // Settings bei Generierung - voice: "de-DE", - speed: 1.0 - } - }, - - // Organisation (optional) - tags: ["roman", "favorit"], - color: "#FF5733", - - // Statistiken (optional) - stats: { - playCount: 5, - totalTime: 3600, // Sekunden - completed: false - }, - - // Was auch immer du später brauchst - notes: "Für die Zugfahrt", - source: "kindle-import" - }, - created_at: "2024-01-01T10:00:00Z", - updated_at: "2024-01-15T10:30:00Z" -} -``` - -## Basis-Operationen - -### Text erstellen - -```javascript -const { data, error } = await supabase.from('texts').insert({ - title: 'Mein Text', - content: 'Inhalt hier...', - data: { - tts: { speed: 1.0, voice: 'de-DE' }, - tags: ['neu'], - }, -}); -``` - -### Alle Texte holen - -```javascript -const { data: texts } = await supabase - .from('texts') - .select('*') - .order('updated_at', { ascending: false }); -``` - -### Nach Tags filtern - -```javascript -const { data: filtered } = await supabase - .from('texts') - .select('*') - .contains('data', { tags: ['favorit'] }); -``` - -### Leseposition updaten - -```javascript -const { error } = await supabase - .from('texts') - .update({ - data: { - ...currentData, - tts: { - ...currentData.tts, - lastPosition: 5678, - lastPlayed: new Date().toISOString(), - }, - }, - }) - .eq('id', textId); -``` - -### Statistiken hochzählen - -```sql --- Als Postgres Funktion für atomare Updates -CREATE OR REPLACE FUNCTION increment_play_count(text_id UUID) -RETURNS void AS $$ -BEGIN - UPDATE texts - SET data = jsonb_set( - jsonb_set( - data, - '{stats,playCount}', - to_jsonb(COALESCE((data->'stats'->>'playCount')::int, 0) + 1) - ), - '{tts,lastPlayed}', - to_jsonb(NOW()) - ) - WHERE id = text_id; -END; -$$ LANGUAGE plpgsql; - --- Aufruf -SELECT increment_play_count('text-uuid-hier'); -``` - -## Supabase Quickstart - -```bash -# 1. Supabase CLI installieren -npm install -g supabase - -# 2. Projekt initialisieren -supabase init - -# 3. Migration erstellen -supabase migration new create_texts_table - -# 4. SQL von oben in die Migration kopieren - -# 5. Migration ausführen -supabase db push -``` - -## React Native Integration - -```javascript -// hooks/useTexts.js -import { useState, useEffect } from 'react'; -import { supabase } from '../lib/supabase'; - -export const useTexts = () => { - const [texts, setTexts] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - fetchTexts(); - }, []); - - const fetchTexts = async () => { - const { data } = await supabase - .from('texts') - .select('*') - .order('updated_at', { ascending: false }); - - setTexts(data || []); - setLoading(false); - }; - - const createText = async (title, content) => { - const { data, error } = await supabase - .from('texts') - .insert({ - title, - content, - data: { tts: { speed: 1.0, voice: 'de-DE' } }, - }) - .select() - .single(); - - if (data) { - setTexts([data, ...texts]); - } - return { data, error }; - }; - - const updatePosition = async (textId, position) => { - const text = texts.find((t) => t.id === textId); - if (!text) return; - - await supabase - .from('texts') - .update({ - data: { - ...text.data, - tts: { - ...text.data.tts, - lastPosition: position, - lastPlayed: new Date().toISOString(), - }, - }, - }) - .eq('id', textId); - }; - - return { texts, loading, createText, updatePosition, refetch: fetchTexts }; -}; -``` - -## Audio-Cache Management - -```javascript -// hooks/useAudioCache.js -import * as FileSystem from 'expo-file-system'; -import * as Speech from 'expo-speech'; -import { Audio } from 'expo-av'; -import { supabase } from '../lib/supabase'; - -const AUDIO_DIR = `${FileSystem.documentDirectory}audio/`; - -export const useAudioCache = () => { - // Verzeichnis erstellen beim Start - useEffect(() => { - FileSystem.makeDirectoryAsync(AUDIO_DIR, { intermediates: true }).catch(() => {}); // Ignorieren wenn bereits existiert - }, []); - - // Text in Chunks aufteilen (z.B. alle 1000 Zeichen) - const chunkText = (text, chunkSize = 1000) => { - const chunks = []; - for (let i = 0; i < text.length; i += chunkSize) { - chunks.push({ - id: `chunk-${chunks.length}`, - start: i, - end: Math.min(i + chunkSize, text.length), - content: text.slice(i, i + chunkSize), - }); - } - return chunks; - }; - - // Audio für einen Chunk generieren und speichern - const generateAudioChunk = async (textId, chunk, settings) => { - const filename = `${textId}-${chunk.id}.mp3`; - const filePath = `${AUDIO_DIR}${filename}`; - - // Option 1: Mit einer TTS API (z.B. Google Cloud TTS) - // const audioData = await callTTSAPI(chunk.content, settings); - // await FileSystem.writeAsStringAsync(filePath, audioData, { - // encoding: FileSystem.EncodingType.Base64 - // }); - - // Option 2: Workaround mit expo-speech (keine direkte MP3 Generierung) - // Hinweis: expo-speech kann nicht direkt als Datei speichern - // Alternative: Web-API oder Cloud-Service nutzen - - const fileInfo = await FileSystem.getInfoAsync(filePath); - - return { - id: chunk.id, - start: chunk.start, - end: chunk.end, - filename, - size: fileInfo.size || 0, - duration: Math.ceil(chunk.content.length / 150) * 60, // Geschätzt - createdAt: new Date().toISOString(), - }; - }; - - // Alle Chunks für einen Text generieren - const generateAudioForText = async (textId, content, settings = {}) => { - const chunks = chunkText(content); - const audioChunks = []; - - for (const chunk of chunks) { - const audioChunk = await generateAudioChunk(textId, chunk, settings); - audioChunks.push(audioChunk); - } - - // Metadaten in Supabase updaten - await updateAudioMetadata(textId, audioChunks, settings); - - return audioChunks; - }; - - // Audio-Metadaten in Supabase speichern - const updateAudioMetadata = async (textId, chunks, settings) => { - const totalSize = chunks.reduce((sum, chunk) => sum + chunk.size, 0); - - const { data: currentText } = await supabase - .from('texts') - .select('data') - .eq('id', textId) - .single(); - - await supabase - .from('texts') - .update({ - data: { - ...currentText.data, - audio: { - hasLocalCache: true, - chunks, - totalSize, - lastGenerated: new Date().toISOString(), - settings, - }, - }, - }) - .eq('id', textId); - }; - - // Audio abspielen - const playAudioFromCache = async (textId, startPosition = 0) => { - const { data: text } = await supabase.from('texts').select('data').eq('id', textId).single(); - - if (!text?.data?.audio?.hasLocalCache) { - throw new Error('Kein Audio-Cache vorhanden'); - } - - // Richtigen Chunk finden - const chunk = text.data.audio.chunks.find( - (c) => startPosition >= c.start && startPosition < c.end - ); - - if (!chunk) return; - - const filePath = `${AUDIO_DIR}${chunk.filename}`; - const { sound } = await Audio.Sound.createAsync({ uri: filePath }); - - // Position innerhalb des Chunks berechnen - const chunkPosition = startPosition - chunk.start; - const positionMillis = (chunkPosition / chunk.end) * chunk.duration * 1000; - - await sound.setPositionAsync(positionMillis); - await sound.playAsync(); - - return sound; - }; - - // Cache löschen - const clearAudioCache = async (textId) => { - const { data: text } = await supabase.from('texts').select('data').eq('id', textId).single(); - - if (text?.data?.audio?.chunks) { - for (const chunk of text.data.audio.chunks) { - try { - await FileSystem.deleteAsync(`${AUDIO_DIR}${chunk.filename}`); - } catch (e) { - console.log('Fehler beim Löschen:', e); - } - } - } - - // Metadaten updaten - await supabase - .from('texts') - .update({ - data: { - ...text.data, - audio: { - hasLocalCache: false, - chunks: [], - totalSize: 0, - }, - }, - }) - .eq('id', textId); - }; - - // Cache-Größe berechnen - const getCacheSize = async () => { - const files = await FileSystem.readDirectoryAsync(AUDIO_DIR); - let totalSize = 0; - - for (const file of files) { - const info = await FileSystem.getInfoAsync(`${AUDIO_DIR}${file}`); - totalSize += info.size || 0; - } - - return totalSize; - }; - - return { - generateAudioForText, - playAudioFromCache, - clearAudioCache, - getCacheSize, - }; -}; -``` - -## Beispiel-Screen für Audio-Management - -```javascript -// screens/TextDetailScreen.js -import React, { useState } from 'react'; -import { View, Text, Button, ActivityIndicator } from 'react-native'; -import { useAudioCache } from '../hooks/useAudioCache'; - -export const TextDetailScreen = ({ route }) => { - const { text } = route.params; - const { generateAudioForText, playAudioFromCache, clearAudioCache } = useAudioCache(); - const [generating, setGenerating] = useState(false); - - const hasCache = text.data?.audio?.hasLocalCache; - - const handleGenerateAudio = async () => { - setGenerating(true); - try { - await generateAudioForText(text.id, text.content, { - voice: text.data?.tts?.voice || 'de-DE', - speed: text.data?.tts?.speed || 1.0, - }); - // Text-Objekt neu laden - } catch (error) { - console.error('Fehler beim Generieren:', error); - } finally { - setGenerating(false); - } - }; - - const handlePlay = async () => { - try { - const position = text.data?.tts?.lastPosition || 0; - await playAudioFromCache(text.id, position); - } catch (error) { - // Fallback zu expo-speech - Speech.speak(text.content.slice(position), { - language: text.data?.tts?.voice || 'de-DE', - rate: text.data?.tts?.speed || 1.0, - }); - } - }; - - return ( - - {text.title} - - {!hasCache && ( -