Sync from upstream llama.cpp repository

This commit is contained in:
2026-01-16 10:43:34 +08:00
parent 3bc369a6f7
commit f4ae4cc7da
2053 changed files with 956010 additions and 1 deletions

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import * as Tooltip from '$lib/components/ui/tooltip';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import Page from '../../../src/routes/+page.svelte';
let sidebarOpen = $state(false);
</script>
<!--
Test wrapper that provides necessary context providers for component testing.
This mirrors the providers from +layout.svelte.
-->
<Tooltip.Provider>
<Sidebar.Provider bind:open={sidebarOpen}>
<Page />
</Sidebar.Provider>
</Tooltip.Provider>

View File

@@ -0,0 +1,11 @@
import { describe, it, expect } from 'vitest';
import { render } from 'vitest-browser-svelte';
import TestWrapper from './components/TestWrapper.svelte';
describe('/+page.svelte', () => {
it('should render page without throwing', async () => {
// Basic smoke test - page should render without throwing errors
// API calls will fail in test environment but component should still mount
expect(() => render(TestWrapper)).not.toThrow();
});
});

View File

@@ -0,0 +1,6 @@
import { expect, test } from '@playwright/test';
test('home page has expected h1', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});

View File

@@ -0,0 +1,161 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import ChatForm from '$lib/components/app/chat/ChatForm/ChatForm.svelte';
import { expect } from 'storybook/test';
import { mockServerProps, mockConfigs } from './fixtures/storybook-mocks';
import jpgAsset from './fixtures/assets/1.jpg?url';
import svgAsset from './fixtures/assets/hf-logo.svg?url';
import pdfAsset from './fixtures/assets/example.pdf?raw';
const { Story } = defineMeta({
title: 'Components/ChatScreen/ChatForm',
component: ChatForm,
parameters: {
layout: 'centered'
}
});
let fileAttachments = $state([
{
id: '1',
name: '1.jpg',
type: 'image/jpeg',
size: 44891,
preview: jpgAsset,
file: new File([''], '1.jpg', { type: 'image/jpeg' })
},
{
id: '2',
name: 'hf-logo.svg',
type: 'image/svg+xml',
size: 1234,
preview: svgAsset,
file: new File([''], 'hf-logo.svg', { type: 'image/svg+xml' })
},
{
id: '3',
name: 'example.pdf',
type: 'application/pdf',
size: 351048,
file: new File([pdfAsset], 'example.pdf', { type: 'application/pdf' })
}
]);
</script>
<Story
name="Default"
args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
play={async ({ canvas, userEvent }) => {
mockServerProps(mockConfigs.noModalities);
const textarea = await canvas.findByRole('textbox');
const submitButton = await canvas.findByRole('button', { name: 'Send' });
// Expect the input to be focused after the component is mounted
await expect(textarea).toHaveFocus();
// Expect the submit button to be disabled
await expect(submitButton).toBeDisabled();
const text = 'What is the meaning of life?';
await userEvent.clear(textarea);
await userEvent.type(textarea, text);
await expect(textarea).toHaveValue(text);
const fileInput = document.querySelector('input[type="file"]');
await expect(fileInput).not.toHaveAttribute('accept');
// Open file attachments dropdown
const fileUploadButton = canvas.getByText('Attach files');
await userEvent.click(fileUploadButton);
// Check dropdown menu items are disabled (no modalities)
const imagesButton = document.querySelector('.images-button');
const audioButton = document.querySelector('.audio-button');
await expect(imagesButton).toHaveAttribute('data-disabled');
await expect(audioButton).toHaveAttribute('data-disabled');
// Close dropdown by pressing Escape
await userEvent.keyboard('{Escape}');
}}
/>
<Story name="Loading" args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]', isLoading: true }} />
<Story
name="VisionModality"
args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
play={async ({ canvas, userEvent }) => {
mockServerProps(mockConfigs.visionOnly);
// Open file attachments dropdown and verify it works
const fileUploadButton = canvas.getByText('Attach files');
await userEvent.click(fileUploadButton);
// Verify dropdown menu items exist
const imagesButton = document.querySelector('.images-button');
const audioButton = document.querySelector('.audio-button');
await expect(imagesButton).toBeInTheDocument();
await expect(audioButton).toBeInTheDocument();
// Close dropdown by pressing Escape
await userEvent.keyboard('{Escape}');
console.log(' Vision modality: Dropdown menu verified');
}}
/>
<Story
name="AudioModality"
args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
play={async ({ canvas, userEvent }) => {
mockServerProps(mockConfigs.audioOnly);
// Open file attachments dropdown and verify it works
const fileUploadButton = canvas.getByText('Attach files');
await userEvent.click(fileUploadButton);
// Verify dropdown menu items exist
const imagesButton = document.querySelector('.images-button');
const audioButton = document.querySelector('.audio-button');
await expect(imagesButton).toBeInTheDocument();
await expect(audioButton).toBeInTheDocument();
// Close dropdown by pressing Escape
await userEvent.keyboard('{Escape}');
console.log(' Audio modality: Dropdown menu verified');
}}
/>
<Story
name="FileAttachments"
args={{
class: 'max-w-[56rem] w-[calc(100vw-2rem)]',
uploadedFiles: fileAttachments
}}
play={async ({ canvas }) => {
mockServerProps(mockConfigs.bothModalities);
const jpgAttachment = canvas.getByAltText('1.jpg');
const svgAttachment = canvas.getByAltText('hf-logo.svg');
const pdfFileExtension = canvas.getByText('PDF');
const pdfAttachment = canvas.getByText('example.pdf');
const pdfSize = canvas.getByText('342.82 KB');
await expect(jpgAttachment).toBeInTheDocument();
await expect(jpgAttachment).toHaveAttribute('src', jpgAsset);
await expect(svgAttachment).toBeInTheDocument();
await expect(svgAttachment).toHaveAttribute('src', svgAsset);
await expect(pdfFileExtension).toBeInTheDocument();
await expect(pdfAttachment).toBeInTheDocument();
await expect(pdfSize).toBeInTheDocument();
}}
/>

View File

@@ -0,0 +1,207 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import ChatMessage from '$lib/components/app/chat/ChatMessages/ChatMessage.svelte';
const { Story } = defineMeta({
title: 'Components/ChatScreen/ChatMessage',
component: ChatMessage,
parameters: {
layout: 'centered'
}
});
// Mock messages for different scenarios
const userMessage: DatabaseMessage = {
id: '1',
convId: 'conv-1',
type: 'message',
timestamp: Date.now() - 1000 * 60 * 5,
role: 'user',
content: 'What is the meaning of life, the universe, and everything?',
parent: '',
thinking: '',
children: []
};
const assistantMessage: DatabaseMessage = {
id: '2',
convId: 'conv-1',
type: 'message',
timestamp: Date.now() - 1000 * 60 * 3,
role: 'assistant',
content:
'The answer to the ultimate question of life, the universe, and everything is **42**.\n\nThis comes from Douglas Adams\' "The Hitchhiker\'s Guide to the Galaxy," where a supercomputer named Deep Thought calculated this answer over 7.5 million years. However, the question itself was never properly formulated, which is why the answer seems meaningless without context.',
parent: '1',
thinking: '',
children: []
};
const assistantWithReasoning: DatabaseMessage = {
id: '3',
convId: 'conv-1',
type: 'message',
timestamp: Date.now() - 1000 * 60 * 2,
role: 'assistant',
content: "Here's the concise answer, now that I've thought it through carefully for you.",
parent: '1',
thinking:
"Let's consider the user's question step by step:\\n\\n1. Identify the core problem\\n2. Evaluate relevant information\\n3. Formulate a clear answer\\n\\nFollowing this process ensures the final response stays focused and accurate.",
children: []
};
const rawOutputMessage: DatabaseMessage = {
id: '6',
convId: 'conv-1',
type: 'message',
timestamp: Date.now() - 1000 * 60,
role: 'assistant',
content:
'<|channel|>analysis<|message|>User greeted me. Initiating overcomplicated analysis: Is this a trap? No, just a normal hello. Respond calmly, act like a helpful assistant, and do not start explaining quantum physics again. Confidence 0.73. Engaging socially acceptable greeting protocol...<|end|>Hello there! How can I help you today?',
parent: '1',
thinking: '',
children: []
};
let processingMessage = $state({
id: '4',
convId: 'conv-1',
type: 'message',
timestamp: 0, // No timestamp = processing
role: 'assistant',
content: '',
parent: '1',
thinking: '',
children: []
});
let streamingMessage = $state({
id: '5',
convId: 'conv-1',
type: 'message',
timestamp: 0, // No timestamp = streaming
role: 'assistant',
content: '',
parent: '1',
thinking: '',
children: []
});
</script>
<Story
name="User"
args={{
message: userMessage
}}
play={async () => {
const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false);
}}
/>
<Story
name="Assistant"
args={{
class: 'max-w-[56rem] w-[calc(100vw-2rem)]',
message: assistantMessage
}}
play={async () => {
const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false);
}}
/>
<Story
name="AssistantWithReasoning"
args={{
class: 'max-w-[56rem] w-[calc(100vw-2rem)]',
message: assistantWithReasoning
}}
play={async () => {
const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false);
}}
/>
<Story
name="RawLlmOutput"
args={{
class: 'max-w-[56rem] w-[calc(100vw-2rem)]',
message: rawOutputMessage
}}
play={async () => {
const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', true);
}}
/>
<Story
name="WithReasoningContent"
args={{
message: streamingMessage
}}
asChild
play={async () => {
const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false);
// Phase 1: Stream reasoning content in chunks
let reasoningText =
'I need to think about this carefully. Let me break down the problem:\n\n1. The user is asking for help with something complex\n2. I should provide a thorough and helpful response\n3. I need to consider multiple approaches\n4. The best solution would be to explain step by step\n\nThis approach will ensure clarity and understanding.';
let reasoningChunk = 'I';
let i = 0;
while (i < reasoningText.length) {
const chunkSize = Math.floor(Math.random() * 5) + 3; // Random 3-7 characters
const chunk = reasoningText.slice(i, i + chunkSize);
reasoningChunk += chunk;
// Update the reactive state directly
streamingMessage.thinking = reasoningChunk;
i += chunkSize;
await new Promise((resolve) => setTimeout(resolve, 50));
}
const regularText =
"Based on my analysis, here's the solution:\n\n**Step 1:** First, we need to understand the requirements clearly.\n\n**Step 2:** Then we can implement the solution systematically.\n\n**Step 3:** Finally, we test and validate the results.\n\nThis approach ensures we cover all aspects of the problem effectively.";
let contentChunk = '';
i = 0;
while (i < regularText.length) {
const chunkSize = Math.floor(Math.random() * 5) + 3; // Random 3-7 characters
const chunk = regularText.slice(i, i + chunkSize);
contentChunk += chunk;
// Update the reactive state directly
streamingMessage.content = contentChunk;
i += chunkSize;
await new Promise((resolve) => setTimeout(resolve, 50));
}
streamingMessage.timestamp = Date.now();
}}
>
<div class="w-[56rem]">
<ChatMessage message={streamingMessage} />
</div>
</Story>
<Story
name="Processing"
args={{
message: processingMessage
}}
play={async () => {
const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false);
// Import the chat store to simulate loading state
const { chatStore } = await import('$lib/stores/chat.svelte');
// Set loading state to true to trigger the processing UI
chatStore.isLoading = true;
// Simulate the processing state hook behavior
// This will show the "Generating..." text and parameter details
await new Promise((resolve) => setTimeout(resolve, 100));
}}
/>

View File

@@ -0,0 +1,19 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { ChatSettings } from '$lib/components/app';
import { fn } from 'storybook/test';
const { Story } = defineMeta({
title: 'Components/ChatSettings',
component: ChatSettings,
parameters: {
layout: 'fullscreen'
},
args: {
onClose: fn(),
onSave: fn()
}
});
</script>
<Story name="Default" />

View File

@@ -0,0 +1,97 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import ChatSidebar from '$lib/components/app/chat/ChatSidebar/ChatSidebar.svelte';
import { waitFor } from 'storybook/test';
import { screen } from 'storybook/test';
const { Story } = defineMeta({
title: 'Components/ChatSidebar',
component: ChatSidebar,
parameters: {
layout: 'centered'
}
});
// Mock conversations for the sidebar
const mockConversations: DatabaseConversation[] = [
{
id: 'conv-1',
name: 'Getting Started with AI',
lastModified: Date.now() - 1000 * 60 * 5, // 5 minutes ago
currNode: 'msg-1'
},
{
id: 'conv-2',
name: 'Python Programming Help',
lastModified: Date.now() - 1000 * 60 * 60 * 2, // 2 hours ago
currNode: 'msg-2'
},
{
id: 'conv-3',
name: 'Creative Writing Ideas',
lastModified: Date.now() - 1000 * 60 * 60 * 24, // 1 day ago
currNode: 'msg-3'
},
{
id: 'conv-4',
name: 'This is a very long conversation title that should be truncated properly when displayed',
lastModified: Date.now() - 1000 * 60 * 60 * 24 * 3, // 3 days ago
currNode: 'msg-4'
},
{
id: 'conv-5',
name: 'Math Problem Solving',
lastModified: Date.now() - 1000 * 60 * 60 * 24 * 7, // 1 week ago
currNode: 'msg-5'
}
];
</script>
<Story
asChild
name="Default"
play={async () => {
const { conversationsStore } = await import('$lib/stores/conversations.svelte');
waitFor(() => setTimeout(() => {
conversationsStore.conversations = mockConversations;
}, 0));
}}
>
<div class="flex-column h-full h-screen w-72 bg-background">
<ChatSidebar />
</div>
</Story>
<Story
asChild
name="SearchActive"
play={async ({ userEvent }) => {
const { conversationsStore } = await import('$lib/stores/conversations.svelte');
waitFor(() => setTimeout(() => {
conversationsStore.conversations = mockConversations;
}, 0));
const searchTrigger = screen.getByText('Search conversations');
userEvent.click(searchTrigger);
}}
>
<div class="flex-column h-full h-screen w-72 bg-background">
<ChatSidebar />
</div>
</Story>
<Story
asChild
name="Empty"
play={async () => {
// Mock empty conversations store
const { conversationsStore } = await import('$lib/stores/conversations.svelte');
conversationsStore.conversations = [];
}}
>
<div class="flex-column h-full h-screen w-72 bg-background">
<ChatSidebar />
</div>
</Story>

View File

@@ -0,0 +1,44 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Introduction" />
# llama.cpp Web UI
Welcome to the **llama.cpp Web UI** component library! This Storybook showcases the components used in the modern web interface for the llama.cpp server.
## 🚀 About This Project
WebUI is a modern web interface for the llama.cpp server, built with SvelteKit and ShadCN UI. Features include:
- **Real-time chat conversations** with AI assistants
- **Multi-conversation management** with persistent storage
- **Advanced parameter tuning** for model behavior
- **File upload support** for multimodal interactions
- **Responsive design** that works on desktop and mobile
## 🎨 Design System
The UI is built using:
- **SvelteKit** - Modern web framework with excellent performance
- **Tailwind CSS** - Utility-first CSS framework for rapid styling
- **ShadCN/UI** - High-quality, accessible component library
- **Lucide Icons** - Beautiful, consistent icon set
## 🔧 Development
This Storybook serves as both documentation and a development environment for the UI components. Each story demonstrates:
- **Component variations** - Different states and configurations
- **Interactive examples** - Live components you can interact with
- **Usage patterns** - How components work together
- **Styling consistency** - Unified design language
## 🚀 Getting Started
To explore the components:
1. **Browse the sidebar** to see all available components
2. **Click on stories** to see different component states
3. **Use the controls panel** to interact with component props
4. **Check the docs tab** for detailed component information

View File

@@ -0,0 +1,130 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import { expect } from 'storybook/test';
import { MarkdownContent } from '$lib/components/app';
import { AI_TUTORIAL_MD } from './fixtures/ai-tutorial.js';
import { API_DOCS_MD } from './fixtures/api-docs.js';
import { BLOG_POST_MD } from './fixtures/blog-post.js';
import { DATA_ANALYSIS_MD } from './fixtures/data-analysis.js';
import { README_MD } from './fixtures/readme.js';
import { MATH_FORMULAS_MD } from './fixtures/math-formulas.js';
import { EMPTY_MD } from './fixtures/empty.js';
const { Story } = defineMeta({
title: 'Components/MarkdownContent',
component: MarkdownContent,
parameters: {
layout: 'centered'
}
});
</script>
<Story name="Empty" args={{ content: EMPTY_MD, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }} />
<Story
name="AI Tutorial"
args={{ content: AI_TUTORIAL_MD, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
/>
<Story
name="API Documentation"
args={{ content: API_DOCS_MD, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
/>
<Story
name="Technical Blog"
args={{ content: BLOG_POST_MD, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
/>
<Story
name="Data Analysis"
args={{ content: DATA_ANALYSIS_MD, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
/>
<Story
name="README file"
args={{ content: README_MD, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
/>
<Story
name="Math Formulas"
args={{ content: MATH_FORMULAS_MD, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
/>
<Story
name="URL Links"
args={{
content: `# URL Links Test
Here are some example URLs that should open in new tabs:
- [Hugging Face Homepage](https://huggingface.co)
- [GitHub Repository](https://github.com/ggml-org/llama.cpp)
- [OpenAI Website](https://openai.com)
- [Google Search](https://www.google.com)
You can also test inline links like https://example.com or https://docs.python.org.
All links should have \`target="_blank"\` and \`rel="noopener noreferrer"\` attributes for security.`,
class: 'max-w-[56rem] w-[calc(100vw-2rem)]'
}}
play={async ({ canvasElement }) => {
// Wait for component to render
await new Promise((resolve) => setTimeout(resolve, 100));
// Find all links in the rendered content
const links = canvasElement.querySelectorAll('a[href]');
// Test that we have the expected number of links
expect(links.length).toBeGreaterThan(0);
// Test each link for proper attributes
links.forEach((link) => {
const href = link.getAttribute('href');
// Test that external links have proper security attributes
if (href && (href.startsWith('http://') || href.startsWith('https://'))) {
expect(link.getAttribute('target')).toBe('_blank');
expect(link.getAttribute('rel')).toBe('noopener noreferrer');
}
});
// Test specific links exist
const hugginFaceLink = Array.from(links).find(
(link) => link.getAttribute('href') === 'https://huggingface.co'
);
expect(hugginFaceLink).toBeTruthy();
expect(hugginFaceLink?.textContent).toBe('Hugging Face Homepage');
const githubLink = Array.from(links).find(
(link) => link.getAttribute('href') === 'https://github.com/ggml-org/llama.cpp'
);
expect(githubLink).toBeTruthy();
expect(githubLink?.textContent).toBe('GitHub Repository');
const openaiLink = Array.from(links).find(
(link) => link.getAttribute('href') === 'https://openai.com'
);
expect(openaiLink).toBeTruthy();
expect(openaiLink?.textContent).toBe('OpenAI Website');
const googleLink = Array.from(links).find(
(link) => link.getAttribute('href') === 'https://www.google.com'
);
expect(googleLink).toBeTruthy();
expect(googleLink?.textContent).toBe('Google Search');
// Test inline links (auto-linked URLs)
const exampleLink = Array.from(links).find(
(link) => link.getAttribute('href') === 'https://example.com'
);
expect(exampleLink).toBeTruthy();
const pythonDocsLink = Array.from(links).find(
(link) => link.getAttribute('href') === 'https://docs.python.org'
);
expect(pythonDocsLink).toBeTruthy();
console.log(`✅ URL Links test passed - Found ${links.length} links with proper attributes`);
}}
/>

View File

@@ -0,0 +1,164 @@
// AI Assistant Tutorial Response
export const AI_TUTORIAL_MD = String.raw`
# Building a Modern Chat Application with SvelteKit
I'll help you create a **production-ready chat application** using SvelteKit, TypeScript, and WebSockets. This implementation includes real-time messaging, user authentication, and message persistence.
## 🚀 Quick Start
First, let's set up the project:
${'```'}bash
npm create svelte@latest chat-app
cd chat-app
npm install
npm install socket.io socket.io-client
npm install @prisma/client prisma
npm run dev
${'```'}
## 📁 Project Structure
${'```'}
chat-app/
├── src/
│ ├── routes/
│ │ ├── +layout.svelte
│ │ ├── +page.svelte
│ │ └── api/
│ │ └── socket/+server.ts
│ ├── lib/
│ │ ├── components/
│ │ │ ├── ChatMessage.svelte
│ │ │ └── ChatInput.svelte
│ │ └── stores/
│ │ └── chat.ts
│ └── app.html
├── prisma/
│ └── schema.prisma
└── package.json
${'```'}
## 💻 Implementation
### WebSocket Server
${'```'}typescript
// src/lib/server/socket.ts
import { Server } from 'socket.io';
import type { ViteDevServer } from 'vite';
export function initializeSocketIO(server: ViteDevServer) {
const io = new Server(server.httpServer || server, {
cors: {
origin: process.env.ORIGIN || 'http://localhost:5173',
credentials: true
}
});
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
socket.on('message', async (data) => {
// Broadcast to all clients
io.emit('new-message', {
id: crypto.randomUUID(),
userId: socket.id,
content: data.content,
timestamp: new Date().toISOString()
});
});
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
return io;
}
${'```'}
### Client Store
${'```'}typescript
// src/lib/stores/chat.ts
import { writable } from 'svelte/store';
import io from 'socket.io-client';
export interface Message {
id: string;
userId: string;
content: string;
timestamp: string;
}
function createChatStore() {
const { subscribe, update } = writable<Message[]>([]);
let socket: ReturnType<typeof io>;
return {
subscribe,
connect: () => {
socket = io('http://localhost:5173');
socket.on('new-message', (message: Message) => {
update(messages => [...messages, message]);
});
},
sendMessage: (content: string) => {
if (socket && content.trim()) {
socket.emit('message', { content });
}
}
};
}
export const chatStore = createChatStore();
${'```'}
## 🎯 Key Features
✅ **Real-time messaging** with WebSockets
✅ **Message persistence** using Prisma + PostgreSQL
✅ **Type-safe** with TypeScript
✅ **Responsive UI** for all devices
✅ **Auto-reconnection** on connection loss
## 📊 Performance Metrics
| Metric | Value |
|--------|-------|
| **Message Latency** | < 50ms |
| **Concurrent Users** | 10,000+ |
| **Messages/Second** | 5,000+ |
| **Uptime** | 99.9% |
## 🔧 Configuration
### Environment Variables
${'```'}env
DATABASE_URL="postgresql://user:password@localhost:5432/chat"
JWT_SECRET="your-secret-key"
REDIS_URL="redis://localhost:6379"
${'```'}
## 🚢 Deployment
Deploy to production using Docker:
${'```'}dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "build"]
${'```'}
---
*Need help? Check the [documentation](https://kit.svelte.dev) or [open an issue](https://github.com/sveltejs/kit/issues)*
`;

View File

@@ -0,0 +1,160 @@
// API Documentation
export const API_DOCS_MD = String.raw`
# REST API Documentation
## 🔐 Authentication
All API requests require authentication using **Bearer tokens**. Include your API key in the Authorization header:
${'```'}http
GET /api/v1/users
Host: api.example.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
${'```'}
## 📍 Endpoints
### Users API
#### **GET** /api/v1/users
Retrieve a paginated list of users.
**Query Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| page | integer | 1 | Page number |
| limit | integer | 20 | Items per page |
| sort | string | "created_at" | Sort field |
| order | string | "desc" | Sort order |
**Response:** 200 OK
${'```'}json
{
"data": [
{
"id": "usr_1234567890",
"email": "user@example.com",
"name": "John Doe",
"role": "admin",
"created_at": "2024-01-15T10:30:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 156,
"pages": 8
}
}
${'```'}
#### **POST** /api/v1/users
Create a new user account.
**Request Body:**
${'```'}json
{
"email": "newuser@example.com",
"password": "SecurePassword123!",
"name": "Jane Smith",
"role": "user"
}
${'```'}
**Response:** 201 Created
${'```'}json
{
"id": "usr_9876543210",
"email": "newuser@example.com",
"name": "Jane Smith",
"role": "user",
"created_at": "2024-01-21T09:15:00Z"
}
${'```'}
### Error Responses
The API returns errors in a consistent format:
${'```'}json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{
"field": "email",
"message": "Email format is invalid"
}
]
}
}
${'```'}
### Rate Limiting
| Tier | Requests/Hour | Burst |
|------|--------------|-------|
| **Free** | 1,000 | 100 |
| **Pro** | 10,000 | 500 |
| **Enterprise** | Unlimited | - |
**Headers:**
- X-RateLimit-Limit
- X-RateLimit-Remaining
- X-RateLimit-Reset
### Webhooks
Configure webhooks to receive real-time events:
${'```'}javascript
// Webhook payload
{
"event": "user.created",
"timestamp": "2024-01-21T09:15:00Z",
"data": {
"id": "usr_9876543210",
"email": "newuser@example.com"
},
"signature": "sha256=abcd1234..."
}
${'```'}
### SDK Examples
**JavaScript/TypeScript:**
${'```'}typescript
import { ApiClient } from '@example/api-sdk';
const client = new ApiClient({
apiKey: process.env.API_KEY
});
const users = await client.users.list({
page: 1,
limit: 20
});
${'```'}
**Python:**
${'```'}python
from example_api import Client
client = Client(api_key=os.environ['API_KEY'])
users = client.users.list(page=1, limit=20)
${'```'}
---
📚 [Full API Reference](https://api.example.com/docs) | 💬 [Support](https://support.example.com)
`;

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,125 @@
// Blog Post Content
export const BLOG_POST_MD = String.raw`
# Understanding Rust's Ownership System
*Published on March 15, 2024 • 8 min read*
Rust's ownership system is one of its most distinctive features, enabling memory safety without garbage collection. In this post, we'll explore how ownership works and why it's revolutionary for systems programming.
## What is Ownership?
Ownership is a set of rules that governs how Rust manages memory. These rules are checked at compile time, ensuring memory safety without runtime overhead.
### The Three Rules of Ownership
1. **Each value has a single owner**
2. **There can only be one owner at a time**
3. **When the owner goes out of scope, the value is dropped**
## Memory Management Without GC
Traditional approaches to memory management:
- **Manual management** (C/C++): Error-prone, leads to bugs
- **Garbage collection** (Java, Python): Runtime overhead
- **Ownership** (Rust): Compile-time safety, zero runtime cost
## Basic Examples
### Variable Scope
${'```'}rust
fn main() {
let s = String::from("hello"); // s comes into scope
// s is valid here
println!("{}", s);
} // s goes out of scope and is dropped
${'```'}
### Move Semantics
${'```'}rust
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
// println!("{}", s1); // ❌ ERROR: s1 is no longer valid
println!("{}", s2); // ✅ OK: s2 owns the string
}
${'```'}
## Borrowing and References
Instead of transferring ownership, you can **borrow** values:
### Immutable References
${'```'}rust
fn calculate_length(s: &String) -> usize {
s.len() // s is a reference, doesn't own the String
}
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Borrow s1
println!("Length of '{}' is {}", s1, len); // s1 still valid
}
${'```'}
### Mutable References
${'```'}rust
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(", world");
println!("{}", r1);
// let r2 = &mut s; // ❌ ERROR: cannot borrow twice
}
${'```'}
## Common Pitfalls
### Dangling References
${'```'}rust
fn dangle() -> &String { // ❌ ERROR: missing lifetime specifier
let s = String::from("hello");
&s // s will be dropped, leaving a dangling reference
}
${'```'}
### ✅ Solution
${'```'}rust
fn no_dangle() -> String {
let s = String::from("hello");
s // Ownership is moved out
}
${'```'}
## Benefits
- ✅ **No null pointer dereferences**
- ✅ **No data races**
- ✅ **No use-after-free**
- ✅ **No memory leaks**
## Conclusion
Rust's ownership system eliminates entire classes of bugs at compile time. While it has a learning curve, the benefits in safety and performance are worth it.
## Further Reading
- [The Rust Book - Ownership](https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html)
- [Rust by Example - Ownership](https://doc.rust-lang.org/rust-by-example/scope/move.html)
- [Rustlings Exercises](https://github.com/rust-lang/rustlings)
---
*Questions? Reach out on [Twitter](https://twitter.com/rustlang) or join the [Rust Discord](https://discord.gg/rust-lang)*
`;

View File

@@ -0,0 +1,124 @@
// Data Analysis Report
export const DATA_ANALYSIS_MD = String.raw`
# Q4 2024 Business Analytics Report
*Executive Summary • Generated on January 15, 2025*
## 📊 Key Performance Indicators
${'```'}
Daily Active Users (DAU): 1.2M (+65% YoY)
Monthly Active Users (MAU): 4.5M (+48% YoY)
User Retention (Day 30): 68% (+12pp YoY)
Average Session Duration: 24min (+35% YoY)
${'```'}
## 🎯 Product Performance
### Feature Adoption Rates
1. **AI Assistant**: 78% of users (↑ from 45%)
2. **Collaboration Tools**: 62% of users (↑ from 38%)
3. **Analytics Dashboard**: 54% of users (↑ from 31%)
4. **Mobile App**: 41% of users (↑ from 22%)
### Customer Satisfaction
| Metric | Q4 2024 | Q3 2024 | Change |
|--------|---------|---------|--------|
| **NPS Score** | 72 | 68 | +4 |
| **CSAT** | 4.6/5 | 4.4/5 | +0.2 |
| **Support Tickets** | 2,340 | 2,890 | -19% |
| **Resolution Time** | 4.2h | 5.1h | -18% |
## 💰 Revenue Metrics
### Monthly Recurring Revenue (MRR)
- **Current MRR**: $2.8M (+42% YoY)
- **New MRR**: $340K
- **Expansion MRR**: $180K
- **Churned MRR**: $95K
- **Net New MRR**: $425K
### Customer Acquisition
${'```'}
Cost per Acquisition (CAC): $127 (-23% YoY)
Customer Lifetime Value: $1,840 (+31% YoY)
LTV:CAC Ratio: 14.5:1
Payback Period: 3.2 months
${'```'}
## 🌍 Geographic Performance
### Revenue by Region
1. **North America**: 45% ($1.26M)
2. **Europe**: 32% ($896K)
3. **Asia-Pacific**: 18% ($504K)
4. **Other**: 5% ($140K)
### Growth Opportunities
- **APAC**: 89% YoY growth potential
- **Latin America**: Emerging market entry
- **Middle East**: Enterprise expansion
## 📱 Channel Performance
### Traffic Sources
| Channel | Sessions | Conversion | Revenue |
|---------|----------|------------|---------|
| **Organic Search** | 45% | 3.2% | $1.1M |
| **Direct** | 28% | 4.1% | $850K |
| **Social Media** | 15% | 2.8% | $420K |
| **Paid Ads** | 12% | 5.5% | $430K |
### Marketing ROI
- **Content Marketing**: 340% ROI
- **Email Campaigns**: 280% ROI
- **Social Media**: 190% ROI
- **Paid Search**: 220% ROI
## 🔍 User Behavior Analysis
### Session Patterns
- **Peak Hours**: 9-11 AM, 2-4 PM EST
- **Mobile Usage**: 67% of sessions
- **Average Pages/Session**: 4.8
- **Bounce Rate**: 23% (↓ from 31%)
### Feature Usage Heatmap
Most used features in order:
1. Dashboard (89% of users)
2. Search (76% of users)
3. Reports (64% of users)
4. Settings (45% of users)
5. Integrations (32% of users)
## 💡 Recommendations
1. **Invest** in AI capabilities (+$2M budget)
2. **Expand** sales team in APAC region
3. **Improve** onboarding to reduce churn
4. **Launch** enterprise security features
## Appendix
### Methodology
Data collected from:
- Internal analytics (Amplitude)
- Customer surveys (n=2,450)
- Financial systems (NetSuite)
- Market research (Gartner)
---
*Report prepared by Data Analytics Team • [View Interactive Dashboard](https://analytics.example.com)*
`;

View File

@@ -0,0 +1,2 @@
// Empty state
export const EMPTY_MD = '';

View File

@@ -0,0 +1,221 @@
/* eslint-disable no-irregular-whitespace */
// Math Formulas Content
export const MATH_FORMULAS_MD = String.raw`
# Mathematical Formulas and Expressions
This document demonstrates various mathematical notation and formulas that can be rendered using LaTeX syntax in markdown.
## Basic Arithmetic
### Addition and Summation
$$\sum_{i=1}^{n} i = \frac{n(n+1)}{2}$$
## Algebra
### Quadratic Formula
The solutions to $ax^2 + bx + c = 0$ are:
$$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$$
### Binomial Theorem
$$(x + y)^n = \sum_{k=0}^{n} \binom{n}{k} x^{n-k} y^k$$
## Calculus
### Derivatives
The derivative of $f(x) = x^n$ is:
$$f'(x) = nx^{n-1}$$
### Integration
$$\int_a^b f(x) \, dx = F(b) - F(a)$$
### Fundamental Theorem of Calculus
$$\frac{d}{dx} \int_a^x f(t) \, dt = f(x)$$
## Linear Algebra
### Matrix Multiplication
If $A$ is an $m \times n$ matrix and $B$ is an $n \times p$ matrix, then:
$$C_{ij} = \sum_{k=1}^{n} A_{ik} B_{kj}$$
### Eigenvalues and Eigenvectors
For a square matrix $A$, if $Av = \lambda v$ for some non-zero vector $v$, then:
- $\lambda$ is an eigenvalue
- $v$ is an eigenvector
## Statistics and Probability
### Normal Distribution
The probability density function is:
$$f(x) = \frac{1}{\sigma\sqrt{2\pi}} e^{-\frac{1}{2}\left(\frac{x-\mu}{\sigma}\right)^2}$$
### Bayes' Theorem
$$P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B)}$$
### Central Limit Theorem
For large $n$, the sample mean $\bar{X}$ is approximately:
$$\bar{X} \sim N\left(\mu, \frac{\sigma^2}{n}\right)$$
## Trigonometry
### Pythagorean Identity
$$\sin^2\theta + \cos^2\theta = 1$$
### Euler's Formula
$$e^{i\theta} = \cos\theta + i\sin\theta$$
### Taylor Series for Sine
$$\sin x = \sum_{n=0}^{\infty} \frac{(-1)^n}{(2n+1)!} x^{2n+1} = x - \frac{x^3}{3!} + \frac{x^5}{5!} - \frac{x^7}{7!} + \cdots$$
## Complex Analysis
### Complex Numbers
A complex number can be written as:
$$z = a + bi = r e^{i\theta}$$
where $r = |z| = \sqrt{a^2 + b^2}$ and $\theta = \arg(z)$
### Cauchy-Riemann Equations
For a function $f(z) = u(x,y) + iv(x,y)$ to be analytic:
$$\frac{\partial u}{\partial x} = \frac{\partial v}{\partial y}, \quad \frac{\partial u}{\partial y} = -\frac{\partial v}{\partial x}$$
## Differential Equations
### First-order Linear ODE
$$\frac{dy}{dx} + P(x)y = Q(x)$$
Solution: $y = e^{-\int P(x)dx}\left[\int Q(x)e^{\int P(x)dx}dx + C\right]$
### Heat Equation
$$\frac{\partial u}{\partial t} = \alpha \frac{\partial^2 u}{\partial x^2}$$
## Number Theory
### Prime Number Theorem
$$\pi(x) \sim \frac{x}{\ln x}$$
where $\pi(x)$ is the number of primes less than or equal to $x$.
### Fermat's Last Theorem
For $n > 2$, there are no positive integers $a$, $b$, and $c$ such that:
$$a^n + b^n = c^n$$
## Set Theory
### De Morgan's Laws
$$\overline{A \cup B} = \overline{A} \cap \overline{B}$$
$$\overline{A \cap B} = \overline{A} \cup \overline{B}$$
## Advanced Topics
### Riemann Zeta Function
$$\zeta(s) = \sum_{n=1}^{\infty} \frac{1}{n^s} = \prod_{p \text{ prime}} \frac{1}{1-p^{-s}}$$
### Maxwell's Equations
$$\nabla \cdot \mathbf{E} = \frac{\rho}{\epsilon_0}$$
$$\nabla \cdot \mathbf{B} = 0$$
$$\nabla \times \mathbf{E} = -\frac{\partial \mathbf{B}}{\partial t}$$
$$\nabla \times \mathbf{B} = \mu_0\mathbf{J} + \mu_0\epsilon_0\frac{\partial \mathbf{E}}{\partial t}$$
### Schrödinger Equation
$$i\hbar\frac{\partial}{\partial t}\Psi(\mathbf{r},t) = \hat{H}\Psi(\mathbf{r},t)$$
## Inline Math Examples
Here are some inline mathematical expressions:
- The golden ratio: $\phi = \frac{1 + \sqrt{5}}{2} \approx 1.618$
- Euler's number: $e = \lim_{n \to \infty} \left(1 + \frac{1}{n}\right)^n$
- Pi: $\pi = 4 \sum_{n=0}^{\infty} \frac{(-1)^n}{2n+1}$
- Square root of 2: $\sqrt{2} = 1.41421356...$
## Fractions and Radicals
Complex fraction: $\frac{\frac{a}{b} + \frac{c}{d}}{\frac{e}{f} - \frac{g}{h}}$
Nested radicals: $\sqrt{2 + \sqrt{3 + \sqrt{4 + \sqrt{5}}}}$
## Summations and Products
### Geometric Series
$$\sum_{n=0}^{\infty} ar^n = \frac{a}{1-r} \quad \text{for } |r| < 1$$
### Product Notation
$$n! = \prod_{k=1}^{n} k$$
### Double Summation
$$\sum_{i=1}^{m} \sum_{j=1}^{n} a_{ij}$$
## Limits
$$\lim_{x \to 0} \frac{\sin x}{x} = 1$$
$$\lim_{n \to \infty} \left(1 + \frac{x}{n}\right)^n = e^x$$
## Further Bracket Styles and Amounts
- \( \mathrm{GL}_2(\mathbb{F}_7) \): Group of invertible matrices with entries in \(\mathbb{F}_7\).
- Some kernel of \(\mathrm{SL}_2(\mathbb{F}_7)\):
\[
\left\{ \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}, \begin{pmatrix} -1 & 0 \\ 0 & -1 \end{pmatrix} \right\} = \{\pm I\}
\]
- Algebra:
\[
x = \frac{-b \pm \sqrt{\,b^{2}-4ac\,}}{2a}
\]
- $100 and $12.99 are amounts, not LaTeX.
- I have $10, $3.99 and $x + y$ and $100x$. The amount is $2,000.
- Emma buys 2 cupcakes for $3 each and 1 cookie for $1.50. How much money does she spend in total?
- Maria has $20. She buys a notebook for $4.75 and a pack of pencils for $3.25. How much change does she receive?
- 1kg の質量は
\[
E = (1\ \text{kg}) \times (3.0 \times 10^8\ \text{m/s})^2 \approx 9.0 \times 10^{16}\ \text{J}
\]
というエネルギーに相当します。これは約 21百万トンの TNT が爆発したときのエネルギーに匹敵します。
- Algebra: \[
x = \frac{-b \pm \sqrt{\,b^{2}-4ac\,}}{2a}
\]
- Algebraic topology, Homotopy Groups of $\mathbb{S}^3$:
$$\pi_n(\mathbb{S}^3) = \begin{cases}
\mathbb{Z} & n = 3 \\
0 & n > 3, n \neq 4 \\
\mathbb{Z}_2 & n = 4 \\
\end{cases}$$
- Spacer preceded by backslash:
\[
\boxed{
\begin{aligned}
N_{\text{att}}^{\text{(MHA)}} &=
h \bigl[\, d_{\text{model}}\;d_{k} + d_{\text{model}}\;d_{v}\, \bigr] && (\text{Q,K,V の重み})\\
&\quad+ h(d_{k}+d_{k}+d_{v}) && (\text{バイアス Q,K,V}\\[4pt]
&\quad+ (h d_{v})\, d_{\text{model}} && (\text{出力射影 }W^{O})\\
&\quad+ d_{\text{model}} && (\text{バイアス }b^{O})
\end{aligned}}
\]
## Formulas in a Table
| Area | Expression | Comment |
|------|------------|---------|
| **Algebra** | \[
x = \frac{-b \pm \sqrt{\,b^{2}-4ac\,}}{2a}
\] | Quadratic formula |
| | \[
(a+b)^{n} = \sum_{k=0}^{n}\binom{n}{k}\,a^{\,n-k}\,b^{\,k}
\] | Binomial theorem |
| | \(\displaystyle \prod_{k=1}^{n}k = n! \) | Factorial definition |
| **Geometry** | \( \mathbf{a}\cdot \mathbf{b} = \|\mathbf{a}\|\,\|\mathbf{b}\|\,\cos\theta \) | Dot product & angle |
## No math (but chemical)
Balanced chemical reaction with states:
\[
\ce{2H2(g) + O2(g) -> 2H2O(l)}
\]
The standard enthalpy change for the reaction is: $\Delta H^\circ = \pu{-572 kJ mol^{-1}}$.
---
*This document showcases various mathematical notation and formulas that can be rendered in markdown using LaTeX syntax.*
`;

View File

@@ -0,0 +1,136 @@
// README Content
export const README_MD = String.raw`
# 🚀 Awesome Web Framework
[![npm version](https://img.shields.io/npm/v/awesome-framework.svg)](https://www.npmjs.com/package/awesome-framework)
[![Build Status](https://github.com/awesome/framework/workflows/CI/badge.svg)](https://github.com/awesome/framework/actions)
[![Coverage](https://codecov.io/gh/awesome/framework/branch/main/graph/badge.svg)](https://codecov.io/gh/awesome/framework)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
> A modern, fast, and flexible web framework for building scalable applications
## ✨ Features
- 🎯 **Type-Safe** - Full TypeScript support out of the box
- ⚡ **Lightning Fast** - Built on Vite for instant HMR
- 📦 **Zero Config** - Works out of the box for most use cases
- 🎨 **Flexible** - Unopinionated with sensible defaults
- 🔧 **Extensible** - Plugin system for custom functionality
- 📱 **Responsive** - Mobile-first approach
- 🌍 **i18n Ready** - Built-in internationalization
- 🔒 **Secure** - Security best practices by default
## 📦 Installation
${'```'}bash
npm install awesome-framework
# or
yarn add awesome-framework
# or
pnpm add awesome-framework
${'```'}
## 🚀 Quick Start
### Create a new project
${'```'}bash
npx create-awesome-app my-app
cd my-app
npm run dev
${'```'}
### Basic Example
${'```'}javascript
import { createApp } from 'awesome-framework';
const app = createApp({
port: 3000,
middleware: ['cors', 'helmet', 'compression']
});
app.get('/', (req, res) => {
res.json({ message: 'Hello World!' });
});
app.listen(() => {
console.log('Server running on http://localhost:3000');
});
${'```'}
## 📖 Documentation
### Core Concepts
- [Getting Started](https://docs.awesome.dev/getting-started)
- [Configuration](https://docs.awesome.dev/configuration)
- [Routing](https://docs.awesome.dev/routing)
- [Middleware](https://docs.awesome.dev/middleware)
- [Database](https://docs.awesome.dev/database)
- [Authentication](https://docs.awesome.dev/authentication)
### Advanced Topics
- [Performance Optimization](https://docs.awesome.dev/performance)
- [Deployment](https://docs.awesome.dev/deployment)
- [Testing](https://docs.awesome.dev/testing)
- [Security](https://docs.awesome.dev/security)
## 🛠️ Development
### Prerequisites
- Node.js >= 18
- pnpm >= 8
### Setup
${'```'}bash
git clone https://github.com/awesome/framework.git
cd framework
pnpm install
pnpm dev
${'```'}
### Testing
${'```'}bash
pnpm test # Run unit tests
pnpm test:e2e # Run end-to-end tests
pnpm test:watch # Run tests in watch mode
${'```'}
## 🤝 Contributing
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
### Contributors
<a href="https://github.com/awesome/framework/graphs/contributors">
<img src="https://contrib.rocks/image?repo=awesome/framework" />
</a>
## 📊 Benchmarks
| Framework | Requests/sec | Latency (ms) | Memory (MB) |
|-----------|-------------|--------------|-------------|
| **Awesome** | **45,230** | **2.1** | **42** |
| Express | 28,450 | 3.5 | 68 |
| Fastify | 41,200 | 2.3 | 48 |
| Koa | 32,100 | 3.1 | 52 |
*Benchmarks performed on MacBook Pro M2, Node.js 20.x*
## 📝 License
MIT © [Awesome Team](https://github.com/awesome)
## 🙏 Acknowledgments
Special thanks to all our sponsors and contributors who make this project possible.
---
**[Website](https://awesome.dev)** • **[Documentation](https://docs.awesome.dev)** • **[Discord](https://discord.gg/awesome)** • **[Twitter](https://twitter.com/awesomeframework)**
`;

View File

@@ -0,0 +1,81 @@
import { serverStore } from '$lib/stores/server.svelte';
import { modelsStore } from '$lib/stores/models.svelte';
/**
* Mock server properties for Storybook testing
* This utility allows setting mock server configurations without polluting production code
*/
export function mockServerProps(props: Partial<ApiLlamaCppServerProps>): void {
// Reset any pointer-events from previous tests (dropdown cleanup)
const body = document.querySelector('body');
if (body) body.style.pointerEvents = '';
// Directly set the props for testing purposes
(serverStore as unknown as { props: ApiLlamaCppServerProps }).props = {
model_path: props.model_path || 'test-model',
modalities: {
vision: props.modalities?.vision ?? false,
audio: props.modalities?.audio ?? false
},
...props
} as ApiLlamaCppServerProps;
// Set router mode role so activeModelId can be set
(serverStore as unknown as { props: ApiLlamaCppServerProps }).props.role = 'ROUTER';
// Also mock modelsStore methods for modality checking
const vision = props.modalities?.vision ?? false;
const audio = props.modalities?.audio ?? false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(modelsStore as any).modelSupportsVision = () => vision;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(modelsStore as any).modelSupportsAudio = () => audio;
// Mock models list with a test model so activeModelId can be resolved
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(modelsStore as any).models = [
{
id: 'test-model',
name: 'Test Model',
model: 'test-model'
}
];
// Mock selectedModelId
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(modelsStore as any).selectedModelId = 'test-model';
}
/**
* Reset server store to clean state for testing
*/
export function resetServerStore(): void {
(serverStore as unknown as { props: ApiLlamaCppServerProps }).props = {
model_path: '',
modalities: {
vision: false,
audio: false
}
} as ApiLlamaCppServerProps;
(serverStore as unknown as { error: string }).error = '';
(serverStore as unknown as { loading: boolean }).loading = false;
}
/**
* Common mock configurations for Storybook stories
*/
export const mockConfigs = {
visionOnly: {
modalities: { vision: true, audio: false }
},
audioOnly: {
modalities: { vision: false, audio: true }
},
bothModalities: {
modalities: { vision: true, audio: true }
},
noModalities: {
modalities: { vision: false, audio: false }
}
} as const;

View File

@@ -0,0 +1,423 @@
import { describe, it, expect } from 'vitest';
import { AttachmentType } from '$lib/enums';
import {
formatMessageForClipboard,
parseClipboardContent,
hasClipboardAttachments
} from '$lib/utils/clipboard';
describe('formatMessageForClipboard', () => {
it('returns plain content when no extras', () => {
const result = formatMessageForClipboard('Hello world', undefined);
expect(result).toBe('Hello world');
});
it('returns plain content when extras is empty array', () => {
const result = formatMessageForClipboard('Hello world', []);
expect(result).toBe('Hello world');
});
it('handles empty string content', () => {
const result = formatMessageForClipboard('', undefined);
expect(result).toBe('');
});
it('returns plain content when extras has only non-text attachments', () => {
const extras = [
{
type: AttachmentType.IMAGE as const,
name: 'image.png',
base64Url: 'data:image/png;base64,...'
}
];
const result = formatMessageForClipboard('Hello world', extras);
expect(result).toBe('Hello world');
});
it('filters non-text attachments and keeps only text ones', () => {
const extras = [
{
type: AttachmentType.IMAGE as const,
name: 'image.png',
base64Url: 'data:image/png;base64,...'
},
{
type: AttachmentType.TEXT as const,
name: 'file.txt',
content: 'Text content'
},
{
type: AttachmentType.PDF as const,
name: 'doc.pdf',
base64Data: 'data:application/pdf;base64,...',
content: 'PDF content',
processedAsImages: false
}
];
const result = formatMessageForClipboard('Hello', extras);
expect(result).toContain('"file.txt"');
expect(result).not.toContain('image.png');
expect(result).not.toContain('doc.pdf');
});
it('formats message with text attachments', () => {
const extras = [
{
type: AttachmentType.TEXT as const,
name: 'file1.txt',
content: 'File 1 content'
},
{
type: AttachmentType.TEXT as const,
name: 'file2.txt',
content: 'File 2 content'
}
];
const result = formatMessageForClipboard('Hello world', extras);
expect(result).toContain('"Hello world"');
expect(result).toContain('"type": "TEXT"');
expect(result).toContain('"name": "file1.txt"');
expect(result).toContain('"content": "File 1 content"');
expect(result).toContain('"name": "file2.txt"');
});
it('handles content with quotes and special characters', () => {
const content = 'Hello "world" with\nnewline';
const extras = [
{
type: AttachmentType.TEXT as const,
name: 'test.txt',
content: 'Test content'
}
];
const result = formatMessageForClipboard(content, extras);
// Should be valid JSON
expect(result.startsWith('"')).toBe(true);
// The content should be properly escaped
const parsed = JSON.parse(result.split('\n')[0]);
expect(parsed).toBe(content);
});
it('converts legacy context type to TEXT type', () => {
const extras = [
{
type: AttachmentType.LEGACY_CONTEXT as const,
name: 'legacy.txt',
content: 'Legacy content'
}
];
const result = formatMessageForClipboard('Hello', extras);
expect(result).toContain('"type": "TEXT"');
expect(result).not.toContain('"context"');
});
it('handles attachment content with special characters', () => {
const extras = [
{
type: AttachmentType.TEXT as const,
name: 'code.js',
content: 'const x = "hello\\nworld";\nconst y = `template ${var}`;'
}
];
const formatted = formatMessageForClipboard('Check this code', extras);
const parsed = parseClipboardContent(formatted);
expect(parsed.textAttachments[0].content).toBe(
'const x = "hello\\nworld";\nconst y = `template ${var}`;'
);
});
it('handles unicode characters in content and attachments', () => {
const extras = [
{
type: AttachmentType.TEXT as const,
name: 'unicode.txt',
content: '日本語テスト 🎉 émojis'
}
];
const formatted = formatMessageForClipboard('Привет мир 👋', extras);
const parsed = parseClipboardContent(formatted);
expect(parsed.message).toBe('Привет мир 👋');
expect(parsed.textAttachments[0].content).toBe('日本語テスト 🎉 émojis');
});
it('formats as plain text when asPlainText is true', () => {
const extras = [
{
type: AttachmentType.TEXT as const,
name: 'file1.txt',
content: 'File 1 content'
},
{
type: AttachmentType.TEXT as const,
name: 'file2.txt',
content: 'File 2 content'
}
];
const result = formatMessageForClipboard('Hello world', extras, true);
expect(result).toBe('Hello world\n\nFile 1 content\n\nFile 2 content');
});
it('returns plain content when asPlainText is true but no attachments', () => {
const result = formatMessageForClipboard('Hello world', [], true);
expect(result).toBe('Hello world');
});
it('plain text mode does not use JSON format', () => {
const extras = [
{
type: AttachmentType.TEXT as const,
name: 'test.txt',
content: 'Test content'
}
];
const result = formatMessageForClipboard('Hello', extras, true);
expect(result).not.toContain('"type"');
expect(result).not.toContain('[');
expect(result).toBe('Hello\n\nTest content');
});
});
describe('parseClipboardContent', () => {
it('returns plain text as message when not in special format', () => {
const result = parseClipboardContent('Hello world');
expect(result.message).toBe('Hello world');
expect(result.textAttachments).toHaveLength(0);
});
it('handles empty string input', () => {
const result = parseClipboardContent('');
expect(result.message).toBe('');
expect(result.textAttachments).toHaveLength(0);
});
it('handles whitespace-only input', () => {
const result = parseClipboardContent(' \n\t ');
expect(result.message).toBe(' \n\t ');
expect(result.textAttachments).toHaveLength(0);
});
it('returns plain text as message when starts with quote but invalid format', () => {
const result = parseClipboardContent('"Unclosed quote');
expect(result.message).toBe('"Unclosed quote');
expect(result.textAttachments).toHaveLength(0);
});
it('returns original text when JSON array is malformed', () => {
const input = '"Hello"\n[invalid json';
const result = parseClipboardContent(input);
expect(result.message).toBe('"Hello"\n[invalid json');
expect(result.textAttachments).toHaveLength(0);
});
it('parses message with text attachments', () => {
const input = `"Hello world"
[
{"type":"TEXT","name":"file1.txt","content":"File 1 content"},
{"type":"TEXT","name":"file2.txt","content":"File 2 content"}
]`;
const result = parseClipboardContent(input);
expect(result.message).toBe('Hello world');
expect(result.textAttachments).toHaveLength(2);
expect(result.textAttachments[0].name).toBe('file1.txt');
expect(result.textAttachments[0].content).toBe('File 1 content');
expect(result.textAttachments[1].name).toBe('file2.txt');
expect(result.textAttachments[1].content).toBe('File 2 content');
});
it('handles escaped quotes in message', () => {
const input = `"Hello \\"world\\" with quotes"
[
{"type":"TEXT","name":"file.txt","content":"test"}
]`;
const result = parseClipboardContent(input);
expect(result.message).toBe('Hello "world" with quotes');
expect(result.textAttachments).toHaveLength(1);
});
it('handles newlines in message', () => {
const input = `"Hello\\nworld"
[
{"type":"TEXT","name":"file.txt","content":"test"}
]`;
const result = parseClipboardContent(input);
expect(result.message).toBe('Hello\nworld');
expect(result.textAttachments).toHaveLength(1);
});
it('returns message only when no array follows', () => {
const input = '"Just a quoted string"';
const result = parseClipboardContent(input);
expect(result.message).toBe('Just a quoted string');
expect(result.textAttachments).toHaveLength(0);
});
it('filters out invalid attachment objects', () => {
const input = `"Hello"
[
{"type":"TEXT","name":"valid.txt","content":"valid"},
{"type":"INVALID","name":"invalid.txt","content":"invalid"},
{"name":"missing-type.txt","content":"missing"},
{"type":"TEXT","content":"missing name"}
]`;
const result = parseClipboardContent(input);
expect(result.message).toBe('Hello');
expect(result.textAttachments).toHaveLength(1);
expect(result.textAttachments[0].name).toBe('valid.txt');
});
it('handles empty attachments array', () => {
const input = '"Hello"\n[]';
const result = parseClipboardContent(input);
expect(result.message).toBe('Hello');
expect(result.textAttachments).toHaveLength(0);
});
it('roundtrips correctly with formatMessageForClipboard', () => {
const originalContent = 'Hello "world" with\nspecial characters';
const originalExtras = [
{
type: AttachmentType.TEXT as const,
name: 'file1.txt',
content: 'Content with\nnewlines and "quotes"'
},
{
type: AttachmentType.TEXT as const,
name: 'file2.txt',
content: 'Another file'
}
];
const formatted = formatMessageForClipboard(originalContent, originalExtras);
const parsed = parseClipboardContent(formatted);
expect(parsed.message).toBe(originalContent);
expect(parsed.textAttachments).toHaveLength(2);
expect(parsed.textAttachments[0].name).toBe('file1.txt');
expect(parsed.textAttachments[0].content).toBe('Content with\nnewlines and "quotes"');
expect(parsed.textAttachments[1].name).toBe('file2.txt');
expect(parsed.textAttachments[1].content).toBe('Another file');
});
});
describe('hasClipboardAttachments', () => {
it('returns false for plain text', () => {
expect(hasClipboardAttachments('Hello world')).toBe(false);
});
it('returns false for empty string', () => {
expect(hasClipboardAttachments('')).toBe(false);
});
it('returns false for quoted string without attachments', () => {
expect(hasClipboardAttachments('"Hello world"')).toBe(false);
});
it('returns true for valid format with attachments', () => {
const input = `"Hello"
[{"type":"TEXT","name":"file.txt","content":"test"}]`;
expect(hasClipboardAttachments(input)).toBe(true);
});
it('returns false for format with empty attachments array', () => {
const input = '"Hello"\n[]';
expect(hasClipboardAttachments(input)).toBe(false);
});
it('returns false for malformed JSON', () => {
expect(hasClipboardAttachments('"Hello"\n[broken')).toBe(false);
});
});
describe('roundtrip edge cases', () => {
it('preserves empty message with attachments', () => {
const extras = [
{
type: AttachmentType.TEXT as const,
name: 'file.txt',
content: 'Content only'
}
];
const formatted = formatMessageForClipboard('', extras);
const parsed = parseClipboardContent(formatted);
expect(parsed.message).toBe('');
expect(parsed.textAttachments).toHaveLength(1);
expect(parsed.textAttachments[0].content).toBe('Content only');
});
it('preserves attachment with empty content', () => {
const extras = [
{
type: AttachmentType.TEXT as const,
name: 'empty.txt',
content: ''
}
];
const formatted = formatMessageForClipboard('Message', extras);
const parsed = parseClipboardContent(formatted);
expect(parsed.message).toBe('Message');
expect(parsed.textAttachments).toHaveLength(1);
expect(parsed.textAttachments[0].content).toBe('');
});
it('preserves multiple backslashes', () => {
const content = 'Path: C:\\\\Users\\\\test\\\\file.txt';
const extras = [
{
type: AttachmentType.TEXT as const,
name: 'path.txt',
content: 'D:\\\\Data\\\\file'
}
];
const formatted = formatMessageForClipboard(content, extras);
const parsed = parseClipboardContent(formatted);
expect(parsed.message).toBe(content);
expect(parsed.textAttachments[0].content).toBe('D:\\\\Data\\\\file');
});
it('preserves tabs and various whitespace', () => {
const content = 'Line1\t\tTabbed\n Spaced\r\nCRLF';
const extras = [
{
type: AttachmentType.TEXT as const,
name: 'whitespace.txt',
content: '\t\t\n\n '
}
];
const formatted = formatMessageForClipboard(content, extras);
const parsed = parseClipboardContent(formatted);
expect(parsed.message).toBe(content);
expect(parsed.textAttachments[0].content).toBe('\t\t\n\n ');
});
});

View File

@@ -0,0 +1,376 @@
/* eslint-disable no-irregular-whitespace */
import { describe, it, expect, test } from 'vitest';
import { maskInlineLaTeX, preprocessLaTeX } from '$lib/utils/latex-protection';
describe('maskInlineLaTeX', () => {
it('should protect LaTeX $x + y$ but not money $3.99', () => {
const latexExpressions: string[] = [];
const input = 'I have $10, $3.99 and $x + y$ and $100x$. The amount is $2,000.';
const output = maskInlineLaTeX(input, latexExpressions);
expect(output).toBe('I have $10, $3.99 and <<LATEX_0>> and <<LATEX_1>>. The amount is $2,000.');
expect(latexExpressions).toEqual(['$x + y$', '$100x$']);
});
it('should ignore money like $5 and $12.99', () => {
const latexExpressions: string[] = [];
const input = 'Prices are $12.99 and $5. Tax?';
const output = maskInlineLaTeX(input, latexExpressions);
expect(output).toBe('Prices are $12.99 and $5. Tax?');
expect(latexExpressions).toEqual([]);
});
it('should protect inline math $a^2 + b^2$ even after text', () => {
const latexExpressions: string[] = [];
const input = 'Pythagorean: $a^2 + b^2 = c^2$.';
const output = maskInlineLaTeX(input, latexExpressions);
expect(output).toBe('Pythagorean: <<LATEX_0>>.');
expect(latexExpressions).toEqual(['$a^2 + b^2 = c^2$']);
});
it('should not protect math that has letter after closing $ (e.g. units)', () => {
const latexExpressions: string[] = [];
const input = 'The cost is $99 and change.';
const output = maskInlineLaTeX(input, latexExpressions);
expect(output).toBe('The cost is $99 and change.');
expect(latexExpressions).toEqual([]);
});
it('should allow $x$ followed by punctuation', () => {
const latexExpressions: string[] = [];
const input = 'We know $x$, right?';
const output = maskInlineLaTeX(input, latexExpressions);
expect(output).toBe('We know <<LATEX_0>>, right?');
expect(latexExpressions).toEqual(['$x$']);
});
it('should work across multiple lines', () => {
const latexExpressions: string[] = [];
const input = `Emma buys cupcakes for $3 each.\nHow much is $x + y$?`;
const output = maskInlineLaTeX(input, latexExpressions);
expect(output).toBe(`Emma buys cupcakes for $3 each.\nHow much is <<LATEX_0>>?`);
expect(latexExpressions).toEqual(['$x + y$']);
});
it('should not protect $100 but protect $matrix$', () => {
const latexExpressions: string[] = [];
const input = '$100 and $\\mathrm{GL}_2(\\mathbb{F}_7)$ are different.';
const output = maskInlineLaTeX(input, latexExpressions);
expect(output).toBe('$100 and <<LATEX_0>> are different.');
expect(latexExpressions).toEqual(['$\\mathrm{GL}_2(\\mathbb{F}_7)$']);
});
it('should skip if $ is followed by digit and alphanumeric after close (money)', () => {
const latexExpressions: string[] = [];
const input = 'I paid $5 quickly.';
const output = maskInlineLaTeX(input, latexExpressions);
expect(output).toBe('I paid $5 quickly.');
expect(latexExpressions).toEqual([]);
});
it('should protect LaTeX even with special chars inside', () => {
const latexExpressions: string[] = [];
const input = 'Consider $\\alpha_1 + \\beta_2$ now.';
const output = maskInlineLaTeX(input, latexExpressions);
expect(output).toBe('Consider <<LATEX_0>> now.');
expect(latexExpressions).toEqual(['$\\alpha_1 + \\beta_2$']);
});
it('short text', () => {
const latexExpressions: string[] = ['$0$'];
const input = '$a$\n$a$ and $b$';
const output = maskInlineLaTeX(input, latexExpressions);
expect(output).toBe('<<LATEX_1>>\n<<LATEX_2>> and <<LATEX_3>>');
expect(latexExpressions).toEqual(['$0$', '$a$', '$a$', '$b$']);
});
it('empty text', () => {
const latexExpressions: string[] = [];
const input = '$\n$$\n';
const output = maskInlineLaTeX(input, latexExpressions);
expect(output).toBe('$\n$$\n');
expect(latexExpressions).toEqual([]);
});
it('LaTeX-spacer preceded by backslash', () => {
const latexExpressions: string[] = [];
const input = `\\[
\\boxed{
\\begin{aligned}
N_{\\text{att}}^{\\text{(MHA)}} &=
h \\bigl[\\, d_{\\text{model}}\\;d_{k} + d_{\\text{model}}\\;d_{v}\\, \\bigr] && (\\text{Q,K,V の重み})\\\\
&\\quad+ h(d_{k}+d_{k}+d_{v}) && (\\text{バイアス Q,K,V}\\\\[4pt]
&\\quad+ (h d_{v})\\, d_{\\text{model}} && (\\text{出力射影 }W^{O})\\\\
&\\quad+ d_{\\text{model}} && (\\text{バイアス }b^{O})
\\end{aligned}}
\\]`;
const output = maskInlineLaTeX(input, latexExpressions);
expect(output).toBe(input);
expect(latexExpressions).toEqual([]);
});
});
describe('preprocessLaTeX', () => {
test('converts inline \\( ... \\) to $...$', () => {
const input =
'\\( \\mathrm{GL}_2(\\mathbb{F}_7) \\): Group of invertible matrices with entries in \\(\\mathbb{F}_7\\).';
const output = preprocessLaTeX(input);
expect(output).toBe(
'$ \\mathrm{GL}_2(\\mathbb{F}_7) $: Group of invertible matrices with entries in $\\mathbb{F}_7$.'
);
});
test("don't inline \\\\( ... \\) to $...$", () => {
const input =
'Chapter 20 of The TeXbook, in source "Definitions\\\\(also called Macros)", containst the formula \\((x_1,\\ldots,x_n)\\).';
const output = preprocessLaTeX(input);
expect(output).toBe(
'Chapter 20 of The TeXbook, in source "Definitions\\\\(also called Macros)", containst the formula $(x_1,\\ldots,x_n)$.'
);
});
test('preserves display math \\[ ... \\] and protects adjacent text', () => {
const input = `Some kernel of \\(\\mathrm{SL}_2(\\mathbb{F}_7)\\):
\\[
\\left\\{ \\begin{pmatrix} 1 & 0 \\\\ 0 & 1 \\end{pmatrix}, \\begin{pmatrix} -1 & 0 \\\\ 0 & -1 \\end{pmatrix} \\right\\} = \\{\\pm I\\}
\\]`;
const output = preprocessLaTeX(input);
expect(output).toBe(`Some kernel of $\\mathrm{SL}_2(\\mathbb{F}_7)$:
$$
\\left\\{ \\begin{pmatrix} 1 & 0 \\\\ 0 & 1 \\end{pmatrix}, \\begin{pmatrix} -1 & 0 \\\\ 0 & -1 \\end{pmatrix} \\right\\} = \\{\\pm I\\}
$$`);
});
test('handles standalone display math equation', () => {
const input = `Algebra:
\\[
x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}
\\]`;
const output = preprocessLaTeX(input);
expect(output).toBe(`Algebra:
$$
x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}
$$`);
});
test('does not interpret currency values as LaTeX', () => {
const input = 'I have $10, $3.99 and $x + y$ and $100x$. The amount is $2,000.';
const output = preprocessLaTeX(input);
expect(output).toBe('I have \\$10, \\$3.99 and $x + y$ and $100x$. The amount is \\$2,000.');
});
test('ignores dollar signs followed by digits (money), but keeps valid math $x + y$', () => {
const input = 'I have $10, $3.99 and $x + y$ and $100x$. The amount is $2,000.';
const output = preprocessLaTeX(input);
expect(output).toBe('I have \\$10, \\$3.99 and $x + y$ and $100x$. The amount is \\$2,000.');
});
test('handles real-world word problems with amounts and no math delimiters', () => {
const input =
'Emma buys 2 cupcakes for $3 each and 1 cookie for $1.50. How much money does she spend in total?';
const output = preprocessLaTeX(input);
expect(output).toBe(
'Emma buys 2 cupcakes for \\$3 each and 1 cookie for \\$1.50. How much money does she spend in total?'
);
});
test('handles decimal amounts in word problem correctly', () => {
const input =
'Maria has $20. She buys a notebook for $4.75 and a pack of pencils for $3.25. How much change does she receive?';
const output = preprocessLaTeX(input);
expect(output).toBe(
'Maria has \\$20. She buys a notebook for \\$4.75 and a pack of pencils for \\$3.25. How much change does she receive?'
);
});
test('preserves display math with surrounding non-ASCII text', () => {
const input = `1kg の質量は
\\[
E = (1\\ \\text{kg}) \\times (3.0 \\times 10^8\\ \\text{m/s})^2 \\approx 9.0 \\times 10^{16}\\ \\text{J}
\\]
というエネルギーに相当します。これは約 21百万トンの TNT が爆発したときのエネルギーに匹敵します。`;
const output = preprocessLaTeX(input);
expect(output).toBe(
`1kg の質量は
$$
E = (1\\ \\text{kg}) \\times (3.0 \\times 10^8\\ \\text{m/s})^2 \\approx 9.0 \\times 10^{16}\\ \\text{J}
$$
というエネルギーに相当します。これは約 21百万トンの TNT が爆発したときのエネルギーに匹敵します。`
);
});
test('LaTeX-spacer preceded by backslash', () => {
const input = `\\[
\\boxed{
\\begin{aligned}
N_{\\text{att}}^{\\text{(MHA)}} &=
h \\bigl[\\, d_{\\text{model}}\\;d_{k} + d_{\\text{model}}\\;d_{v}\\, \\bigr] && (\\text{Q,K,V の重み})\\\\
&\\quad+ h(d_{k}+d_{k}+d_{v}) && (\\text{バイアス Q,K,V}\\\\[4pt]
&\\quad+ (h d_{v})\\, d_{\\text{model}} && (\\text{出力射影 }W^{O})\\\\
&\\quad+ d_{\\text{model}} && (\\text{バイアス }b^{O})
\\end{aligned}}
\\]`;
const output = preprocessLaTeX(input);
expect(output).toBe(
`$$
\\boxed{
\\begin{aligned}
N_{\\text{att}}^{\\text{(MHA)}} &=
h \\bigl[\\, d_{\\text{model}}\\;d_{k} + d_{\\text{model}}\\;d_{v}\\, \\bigr] && (\\text{Q,K,V の重み})\\\\
&\\quad+ h(d_{k}+d_{k}+d_{v}) && (\\text{バイアス Q,K,V}\\\\[4pt]
&\\quad+ (h d_{v})\\, d_{\\text{model}} && (\\text{出力射影 }W^{O})\\\\
&\\quad+ d_{\\text{model}} && (\\text{バイアス }b^{O})
\\end{aligned}}
$$`
);
});
test('converts \\[ ... \\] even when preceded by text without space', () => {
const input = 'Some line ...\nAlgebra: \\[x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}\\]';
const output = preprocessLaTeX(input);
expect(output).toBe(
'Some line ...\nAlgebra: \n$$x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}$$\n'
);
});
test('converts \\[ ... \\] in table-cells', () => {
const input = `| ID | Expression |\n| #1 | \\[
x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}
\\] |`;
const output = preprocessLaTeX(input);
expect(output).toBe(
'| ID | Expression |\n| #1 | $x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}$ |'
);
});
test('escapes isolated $ before digits ($5 → \\$5), but not valid math', () => {
const input = 'This costs $5 and this is math $x^2$. $100 is money.';
const output = preprocessLaTeX(input);
expect(output).toBe('This costs \\$5 and this is math $x^2$. \\$100 is money.');
// Note: Since $x^2$ is detected as valid LaTeX, it's preserved.
// $5 becomes \$5 only *after* real math is masked — but here it's correct because the masking logic avoids treating $5 as math.
});
test('display with LaTeX-line-breaks', () => {
const input = String.raw`- Algebraic topology, Homotopy Groups of $\mathbb{S}^3$:
$$\pi_n(\mathbb{S}^3) = \begin{cases}
\mathbb{Z} & n = 3 \\
0 & n > 3, n \neq 4 \\
\mathbb{Z}_2 & n = 4 \\
\end{cases}$$`;
const output = preprocessLaTeX(input);
// If the formula contains '\\' the $$-delimiters should be in their own line.
expect(output).toBe(`- Algebraic topology, Homotopy Groups of $\\mathbb{S}^3$:
$$\n\\pi_n(\\mathbb{S}^3) = \\begin{cases}
\\mathbb{Z} & n = 3 \\\\
0 & n > 3, n \\neq 4 \\\\
\\mathbb{Z}_2 & n = 4 \\\\
\\end{cases}\n$$`);
});
test('handles mhchem notation safely if present', () => {
const input = 'Chemical reaction: \\( \\ce{H2O} \\) and $\\ce{CO2}$';
const output = preprocessLaTeX(input);
expect(output).toBe('Chemical reaction: $ \\ce{H2O} $ and $\\ce{CO2}$');
});
test('preserves code blocks', () => {
const input = 'Inline code: `sum $total` and block:\n```\ndollar $amount\n```\nEnd.';
const output = preprocessLaTeX(input);
expect(output).toBe(input); // Code blocks prevent misinterpretation
});
test('preserves backslash parentheses in code blocks (GitHub issue)', () => {
const input = '```python\nfoo = "\\(bar\\)"\n```';
const output = preprocessLaTeX(input);
expect(output).toBe(input); // Code blocks should not have LaTeX conversion applied
});
test('preserves backslash brackets in code blocks', () => {
const input = '```python\nfoo = "\\[bar\\]"\n```';
const output = preprocessLaTeX(input);
expect(output).toBe(input); // Code blocks should not have LaTeX conversion applied
});
test('preserves backslash parentheses in inline code', () => {
const input = 'Use `foo = "\\(bar\\)"` in your code.';
const output = preprocessLaTeX(input);
expect(output).toBe(input);
});
test('escape backslash in mchem ce', () => {
const input = 'mchem ce:\n$\\ce{2H2(g) + O2(g) -> 2H2O(l)}$';
const output = preprocessLaTeX(input);
// mhchem-escape would insert a backslash here.
expect(output).toBe('mchem ce:\n$\\ce{2H2(g) + O2(g) -> 2H2O(l)}$');
});
test('escape backslash in mchem pu', () => {
const input = 'mchem pu:\n$\\pu{-572 kJ mol^{-1}}$';
const output = preprocessLaTeX(input);
// mhchem-escape would insert a backslash here.
expect(output).toBe('mchem pu:\n$\\pu{-572 kJ mol^{-1}}$');
});
test('LaTeX in blockquotes with display math', () => {
const input =
'> **Definition (limit):** \n> \\[\n> \\lim_{x\\to a} f(x) = L\n> \\]\n> means that as \\(x\\) gets close to \\(a\\).';
const output = preprocessLaTeX(input);
// Blockquote markers should be preserved, LaTeX should be converted
expect(output).toContain('> **Definition (limit):**');
expect(output).toContain('$$');
expect(output).toContain('$x$');
expect(output).not.toContain('\\[');
expect(output).not.toContain('\\]');
expect(output).not.toContain('\\(');
expect(output).not.toContain('\\)');
});
test('LaTeX in blockquotes with inline math', () => {
const input =
"> The derivative \\(f'(x)\\) at point \\(x=a\\) measures slope.\n> Formula: \\(f'(a)=\\lim_{h\\to 0}\\frac{f(a+h)-f(a)}{h}\\)";
const output = preprocessLaTeX(input);
// Blockquote markers should be preserved, inline LaTeX converted to $...$
expect(output).toContain("> The derivative $f'(x)$ at point $x=a$ measures slope.");
expect(output).toContain("> Formula: $f'(a)=\\lim_{h\\to 0}\\frac{f(a+h)-f(a)}{h}$");
});
test('Mixed content with blockquotes and regular text', () => {
const input =
'Regular text with \\(x^2\\).\n\n> Quote with \\(y^2\\).\n\nMore text with \\(z^2\\).';
const output = preprocessLaTeX(input);
// All LaTeX should be converted, blockquote markers preserved
expect(output).toBe('Regular text with $x^2$.\n\n> Quote with $y^2$.\n\nMore text with $z^2$.');
});
});

View File

@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest';
import { isValidModelName, normalizeModelName } from '$lib/utils/model-names';
describe('normalizeModelName', () => {
it('preserves Hugging Face org/model format (single slash)', () => {
// Single slash is treated as Hugging Face format and preserved
expect(normalizeModelName('meta-llama/Llama-3.1-8B')).toBe('meta-llama/Llama-3.1-8B');
expect(normalizeModelName('models/model-name-1')).toBe('models/model-name-1');
});
it('extracts filename from multi-segment paths', () => {
// Multiple slashes -> extract just the filename
expect(normalizeModelName('path/to/model/model-name-2')).toBe('model-name-2');
expect(normalizeModelName('/absolute/path/to/model')).toBe('model');
});
it('extracts filename from backslash paths', () => {
expect(normalizeModelName('C\\Models\\model-name-1')).toBe('model-name-1');
expect(normalizeModelName('path\\to\\model\\model-name-2')).toBe('model-name-2');
});
it('handles mixed path separators', () => {
expect(normalizeModelName('path/to\\model/model-name-2')).toBe('model-name-2');
});
it('returns simple names as-is', () => {
expect(normalizeModelName('simple-model')).toBe('simple-model');
expect(normalizeModelName('model-name-2')).toBe('model-name-2');
});
it('trims whitespace', () => {
expect(normalizeModelName(' model-name ')).toBe('model-name');
});
it('returns empty string for empty input', () => {
expect(normalizeModelName('')).toBe('');
expect(normalizeModelName(' ')).toBe('');
});
});
describe('isValidModelName', () => {
it('returns true for valid names', () => {
expect(isValidModelName('model')).toBe(true);
expect(isValidModelName('path/to/model.bin')).toBe(true);
});
it('returns false for empty values', () => {
expect(isValidModelName('')).toBe(false);
expect(isValidModelName(' ')).toBe(false);
});
});