SvelteKit-based WebUI (#14839)
This commit is contained in:
committed by
GitHub
parent
8f8f2274ee
commit
a7a98e0fff
182
tools/server/webui/src/stories/ChatForm.stories.svelte
Normal file
182
tools/server/webui/src/stories/ChatForm.stories.svelte
Normal file
@@ -0,0 +1,182 @@
|
||||
<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/internal/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"]');
|
||||
const acceptAttr = fileInput?.getAttribute('accept');
|
||||
await expect(fileInput).toHaveAttribute('accept');
|
||||
await expect(acceptAttr).not.toContain('image/');
|
||||
await expect(acceptAttr).not.toContain('audio/');
|
||||
|
||||
const fileUploadButton = canvas.getByText('Attach files');
|
||||
|
||||
await userEvent.click(fileUploadButton);
|
||||
|
||||
const recordButton = canvas.getAllByRole('button', { name: 'Start recording' })[1];
|
||||
const imagesButton = document.querySelector('.images-button');
|
||||
const audioButton = document.querySelector('.audio-button');
|
||||
|
||||
await expect(recordButton).toBeDisabled();
|
||||
await expect(imagesButton).toHaveAttribute('data-disabled');
|
||||
await expect(audioButton).toHaveAttribute('data-disabled');
|
||||
}}
|
||||
/>
|
||||
|
||||
<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);
|
||||
|
||||
// Test initial file input state (should accept images but not audio)
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
const acceptAttr = fileInput?.getAttribute('accept');
|
||||
console.log('Vision modality accept attr:', acceptAttr);
|
||||
|
||||
const fileUploadButton = canvas.getByText('Attach files');
|
||||
await userEvent.click(fileUploadButton);
|
||||
|
||||
// Test that record button is disabled (no audio support)
|
||||
const recordButton = canvas.getAllByRole('button', { name: 'Start recording' })[1];
|
||||
await expect(recordButton).toBeDisabled();
|
||||
|
||||
// Test that Images button is enabled (vision support)
|
||||
const imagesButton = document.querySelector('.images-button');
|
||||
await expect(imagesButton).not.toHaveAttribute('data-disabled');
|
||||
|
||||
// Test that Audio button is disabled (no audio support)
|
||||
const audioButton = document.querySelector('.audio-button');
|
||||
await expect(audioButton).toHaveAttribute('data-disabled');
|
||||
|
||||
// Fix for dropdown menu side effect
|
||||
const body = document.querySelector('body');
|
||||
if (body) body.style.pointerEvents = 'all';
|
||||
|
||||
console.log('✅ Vision modality: Images enabled, Audio/Recording disabled');
|
||||
}}
|
||||
/>
|
||||
|
||||
<Story
|
||||
name="AudioModality"
|
||||
args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
|
||||
play={async ({ canvas, userEvent }) => {
|
||||
mockServerProps(mockConfigs.audioOnly);
|
||||
|
||||
// Test initial file input state (should accept audio but not images)
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
const acceptAttr = fileInput?.getAttribute('accept');
|
||||
console.log('Audio modality accept attr:', acceptAttr);
|
||||
|
||||
const fileUploadButton = canvas.getByText('Attach files');
|
||||
await userEvent.click(fileUploadButton);
|
||||
|
||||
// Test that record button is enabled (audio support)
|
||||
const recordButton = canvas.getAllByRole('button', { name: 'Start recording' })[1];
|
||||
await expect(recordButton).not.toBeDisabled();
|
||||
|
||||
// Test that Images button is disabled (no vision support)
|
||||
const imagesButton = document.querySelector('.images-button');
|
||||
await expect(imagesButton).toHaveAttribute('data-disabled');
|
||||
|
||||
// Test that Audio button is enabled (audio support)
|
||||
const audioButton = document.querySelector('.audio-button');
|
||||
await expect(audioButton).not.toHaveAttribute('data-disabled');
|
||||
|
||||
// Fix for dropdown menu side effect
|
||||
const body = document.querySelector('body');
|
||||
if (body) body.style.pointerEvents = 'all';
|
||||
|
||||
console.log('✅ Audio modality: Audio/Recording enabled, Images disabled');
|
||||
}}
|
||||
/>
|
||||
|
||||
<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();
|
||||
}}
|
||||
/>
|
||||
146
tools/server/webui/src/stories/ChatMessage.stories.svelte
Normal file
146
tools/server/webui/src/stories/ChatMessage.stories.svelte
Normal file
@@ -0,0 +1,146 @@
|
||||
<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: []
|
||||
};
|
||||
|
||||
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
|
||||
}}
|
||||
/>
|
||||
|
||||
<Story
|
||||
name="Assistant"
|
||||
args={{
|
||||
class: 'max-w-[56rem] w-[calc(100vw-2rem)]',
|
||||
message: assistantMessage
|
||||
}}
|
||||
/>
|
||||
|
||||
<Story
|
||||
name="WithThinkingBlock"
|
||||
args={{
|
||||
message: streamingMessage
|
||||
}}
|
||||
asChild
|
||||
play={async () => {
|
||||
// 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 () => {
|
||||
// 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));
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import { ChatSettingsDialog } from '$lib/components/app';
|
||||
import { fn } from 'storybook/test';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/ChatSettingsDialog',
|
||||
component: ChatSettingsDialog,
|
||||
parameters: {
|
||||
layout: 'fullscreen'
|
||||
},
|
||||
argTypes: {
|
||||
open: {
|
||||
control: 'boolean',
|
||||
description: 'Whether the dialog is open'
|
||||
}
|
||||
},
|
||||
args: {
|
||||
onOpenChange: fn()
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Open" args={{ open: true }} />
|
||||
|
||||
<Story name="Closed" args={{ open: false }} />
|
||||
97
tools/server/webui/src/stories/ChatSidebar.stories.svelte
Normal file
97
tools/server/webui/src/stories/ChatSidebar.stories.svelte
Normal 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/internal/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 { chatStore } = await import('$lib/stores/chat.svelte');
|
||||
|
||||
waitFor(() => setTimeout(() => {
|
||||
chatStore.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 { chatStore } = await import('$lib/stores/chat.svelte');
|
||||
|
||||
waitFor(() => setTimeout(() => {
|
||||
chatStore.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 { chatStore } = await import('$lib/stores/chat.svelte');
|
||||
chatStore.conversations = [];
|
||||
}}
|
||||
>
|
||||
<div class="flex-column h-full h-screen w-72 bg-background">
|
||||
<ChatSidebar />
|
||||
</div>
|
||||
</Story>
|
||||
44
tools/server/webui/src/stories/Introduction.mdx
Normal file
44
tools/server/webui/src/stories/Introduction.mdx
Normal 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
|
||||
131
tools/server/webui/src/stories/MarkdownContent.stories.svelte
Normal file
131
tools/server/webui/src/stories/MarkdownContent.stories.svelte
Normal file
@@ -0,0 +1,131 @@
|
||||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
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 }) => {
|
||||
const { expect } = await import('storybook/internal/test');
|
||||
|
||||
// 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`);
|
||||
}}
|
||||
/>
|
||||
164
tools/server/webui/src/stories/fixtures/ai-tutorial.ts
Normal file
164
tools/server/webui/src/stories/fixtures/ai-tutorial.ts
Normal 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)*
|
||||
`;
|
||||
160
tools/server/webui/src/stories/fixtures/api-docs.ts
Normal file
160
tools/server/webui/src/stories/fixtures/api-docs.ts
Normal 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)
|
||||
`;
|
||||
BIN
tools/server/webui/src/stories/fixtures/assets/1.jpg
Normal file
BIN
tools/server/webui/src/stories/fixtures/assets/1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 798 KiB |
BIN
tools/server/webui/src/stories/fixtures/assets/example.pdf
Normal file
BIN
tools/server/webui/src/stories/fixtures/assets/example.pdf
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 34 KiB |
125
tools/server/webui/src/stories/fixtures/blog-post.ts
Normal file
125
tools/server/webui/src/stories/fixtures/blog-post.ts
Normal 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)*
|
||||
`;
|
||||
124
tools/server/webui/src/stories/fixtures/data-analysis.ts
Normal file
124
tools/server/webui/src/stories/fixtures/data-analysis.ts
Normal 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)*
|
||||
`;
|
||||
2
tools/server/webui/src/stories/fixtures/empty.ts
Normal file
2
tools/server/webui/src/stories/fixtures/empty.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Empty state
|
||||
export const EMPTY_MD = '';
|
||||
156
tools/server/webui/src/stories/fixtures/math-formulas.ts
Normal file
156
tools/server/webui/src/stories/fixtures/math-formulas.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
// 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$$
|
||||
|
||||
---
|
||||
|
||||
*This document showcases various mathematical notation and formulas that can be rendered in markdown using LaTeX syntax.*
|
||||
`;
|
||||
136
tools/server/webui/src/stories/fixtures/readme.ts
Normal file
136
tools/server/webui/src/stories/fixtures/readme.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
// README Content
|
||||
export const README_MD = String.raw`
|
||||
# 🚀 Awesome Web Framework
|
||||
|
||||
[](https://www.npmjs.com/package/awesome-framework)
|
||||
[](https://github.com/awesome/framework/actions)
|
||||
[](https://codecov.io/gh/awesome/framework)
|
||||
[](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)**
|
||||
`;
|
||||
50
tools/server/webui/src/stories/fixtures/storybook-mocks.ts
Normal file
50
tools/server/webui/src/stories/fixtures/storybook-mocks.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { serverStore } from '$lib/stores/server.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 {
|
||||
// Directly set the private _serverProps for testing purposes
|
||||
(serverStore as unknown as { _serverProps: ApiLlamaCppServerProps })._serverProps = {
|
||||
model_path: props.model_path || 'test-model',
|
||||
modalities: {
|
||||
vision: props.modalities?.vision ?? false,
|
||||
audio: props.modalities?.audio ?? false
|
||||
},
|
||||
...props
|
||||
} as ApiLlamaCppServerProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset server store to clean state for testing
|
||||
*/
|
||||
export function resetServerStore(): void {
|
||||
(serverStore as unknown as { _serverProps: ApiLlamaCppServerProps })._serverProps = {
|
||||
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;
|
||||
Reference in New Issue
Block a user