| 
1 | 1 | <script module lang="ts">  | 
2 | 2 | 	import { defineMeta } from '@storybook/addon-svelte-csf';  | 
3 | 3 | 	import ChatMessage from '$lib/components/app/chat/ChatMessages/ChatMessage.svelte';  | 
 | 4 | +	import { expect } from 'storybook/internal/test';  | 
4 | 5 | 
  | 
5 | 6 | 	const { Story } = defineMeta({  | 
6 | 7 | 		title: 'Components/ChatScreen/ChatMessage',  | 
 | 
47 | 48 | 		children: []  | 
48 | 49 | 	};  | 
49 | 50 | 
  | 
50 |  | -	const processingMessage: DatabaseMessage = {  | 
 | 51 | +	let processingMessage = $state({  | 
51 | 52 | 		id: '4',  | 
52 | 53 | 		convId: 'conv-1',  | 
53 | 54 | 		type: 'message',  | 
54 |  | -		timestamp: Date.now(),  | 
 | 55 | +		timestamp: 0, // No timestamp = processing  | 
55 | 56 | 		role: 'assistant',  | 
56 | 57 | 		content: '',  | 
57 | 58 | 		parent: '1',  | 
58 | 59 | 		thinking: '',  | 
59 | 60 | 		children: []  | 
60 |  | -	};  | 
 | 61 | +	});  | 
 | 62 | +
  | 
 | 63 | +	let streamingMessage = $state({  | 
 | 64 | +		id: '5',  | 
 | 65 | +		convId: 'conv-1',  | 
 | 66 | +		type: 'message',  | 
 | 67 | +		timestamp: 0, // No timestamp = streaming  | 
 | 68 | +		role: 'assistant',  | 
 | 69 | +		content: '',  | 
 | 70 | +		parent: '1',  | 
 | 71 | +		thinking: '',  | 
 | 72 | +		children: []  | 
 | 73 | +	});  | 
61 | 74 | </script>  | 
62 | 75 | 
 
  | 
63 | 76 | <Story  | 
 | 
70 | 83 | <Story  | 
71 | 84 | 	name="Assistant"  | 
72 | 85 | 	args={{  | 
 | 86 | +		class: 'max-w-[56rem] w-[calc(100vw-2rem)]',  | 
73 | 87 | 		message: assistantMessage  | 
74 | 88 | 	}}  | 
75 | 89 | />  | 
76 | 90 | 
 
  | 
77 | 91 | <Story  | 
78 |  | -	name="ThinkingBlock"  | 
 | 92 | +	name="WithThinkingBlock"  | 
 | 93 | +	args={{  | 
 | 94 | +		message: streamingMessage  | 
 | 95 | +	}}  | 
 | 96 | +	asChild  | 
 | 97 | +	play={async ({ canvas }) => {  | 
 | 98 | +		// Phase 1: Stream reasoning content in chunks  | 
 | 99 | +		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.";  | 
 | 100 | +		  | 
 | 101 | +		  | 
 | 102 | +		let reasoningChunk = 'I';  | 
 | 103 | +		let i = 0;  | 
 | 104 | +		while (i < reasoningText.length) {  | 
 | 105 | +			const chunkSize = Math.floor(Math.random() * 5) + 3; // Random 3-7 characters  | 
 | 106 | +			const chunk = reasoningText.slice(i, i + chunkSize);  | 
 | 107 | +			reasoningChunk += chunk;  | 
 | 108 | +			  | 
 | 109 | +			// Update the reactive state directly  | 
 | 110 | +			streamingMessage.thinking = reasoningChunk;  | 
 | 111 | +			  | 
 | 112 | +			i += chunkSize;  | 
 | 113 | +			await new Promise(resolve => setTimeout(resolve, 50));  | 
 | 114 | +		}  | 
 | 115 | +		  | 
 | 116 | +		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.";  | 
 | 117 | +		  | 
 | 118 | +		let contentChunk = '';  | 
 | 119 | +		i = 0;  | 
 | 120 | + | 
 | 121 | +		while (i < regularText.length) {  | 
 | 122 | +			const chunkSize = Math.floor(Math.random() * 5) + 3; // Random 3-7 characters  | 
 | 123 | +			const chunk = regularText.slice(i, i + chunkSize);  | 
 | 124 | +			contentChunk += chunk;  | 
 | 125 | +			  | 
 | 126 | +			// Update the reactive state directly  | 
 | 127 | +			streamingMessage.content = contentChunk;  | 
 | 128 | +			  | 
 | 129 | +			i += chunkSize;  | 
 | 130 | +			await new Promise(resolve => setTimeout(resolve, 50));  | 
 | 131 | +		}  | 
 | 132 | +		  | 
 | 133 | +		streamingMessage.timestamp = Date.now();  | 
 | 134 | + | 
 | 135 | + | 
 | 136 | +		// const collapsibleTrigger = canvas.getByText('Reasoning');  | 
 | 137 | +		// expect(collapsibleTrigger).not.toHaveAttribute('data-state', 'open');  | 
 | 138 | +	}}  | 
 | 139 | +>  | 
 | 140 | + | 
 | 141 | +	<div class="w-[56rem]">  | 
 | 142 | +		<ChatMessage  | 
 | 143 | +		message={streamingMessage}  | 
 | 144 | +	/>  | 
 | 145 | +	</div>  | 
 | 146 | +</Story>  | 
 | 147 | + | 
 | 148 | +<Story  | 
 | 149 | +	name="Processing"  | 
79 | 150 | 	args={{  | 
80 |  | -		message: thinkingMessage  | 
 | 151 | +		message: processingMessage  | 
 | 152 | +	}}  | 
 | 153 | +	play={async ({ canvas }) => {  | 
 | 154 | +		// Import the chat store to simulate loading state  | 
 | 155 | +		const { chatStore } = await import('$lib/stores/chat.svelte');  | 
 | 156 | +		  | 
 | 157 | +		// Set loading state to true to trigger the processing UI  | 
 | 158 | +		chatStore.isLoading = true;  | 
 | 159 | +		  | 
 | 160 | +		// Simulate the processing state hook behavior  | 
 | 161 | +		// This will show the "Generating..." text and parameter details  | 
 | 162 | +		await new Promise(resolve => setTimeout(resolve, 100));  | 
81 | 163 | 	}}  | 
82 | 164 | />  | 
83 | 165 | 
 
  | 
84 | 166 | <Story  | 
85 |  | -	name="ProcessingState"  | 
 | 167 | +	name="Processing with Slots"  | 
86 | 168 | 	args={{  | 
87 | 169 | 		message: processingMessage  | 
88 | 170 | 	}}  | 
89 |  | -	play={({ canvasElement }) => {  | 
90 |  | -		// Simulate processing state by setting up mock processing data  | 
91 |  | -		const processingState = {  | 
92 |  | -			slots: {  | 
93 |  | -				'slot-1': { content: 'Processing your request...', timestamp: Date.now() },  | 
94 |  | -				'slot-2': { content: 'Analyzing data...', timestamp: Date.now() + 1000 }  | 
95 |  | -			}  | 
 | 171 | +	play={async () => {  | 
 | 172 | +		  | 
 | 173 | +		// Import the chat store and slots service to simulate loading state with slots data  | 
 | 174 | +		const { chatStore } = await import('$lib/stores/chat.svelte');  | 
 | 175 | +		const { slotsService } = await import('$lib/services/slots');  | 
 | 176 | +		  | 
 | 177 | +		// Set loading state to true to trigger the processing UI  | 
 | 178 | +		chatStore.isLoading = true;  | 
 | 179 | +		  | 
 | 180 | +		// Mock the slots service to provide realistic slot data  | 
 | 181 | +		const mockProcessingState = {  | 
 | 182 | +			status: 'generating' as const,  | 
 | 183 | +			tokensDecoded: 410,  | 
 | 184 | +			tokensRemaining: 1000,  | 
 | 185 | +			contextUsed: 429,  | 
 | 186 | +			contextTotal: 4096,  | 
 | 187 | +			temperature: 0.8,  | 
 | 188 | +			topP: 0.95,  | 
 | 189 | +			speculative: false,  | 
 | 190 | +			hasNextToken: true  | 
96 | 191 | 		};  | 
97 | 192 | 
 
  | 
98 |  | -		// This would normally be handled by the useProcessingState hook  | 
99 |  | -		// but for Storybook we can simulate the visual state  | 
 | 193 | +		// Override the parseProcessingState method to return our mock data  | 
 | 194 | +		const originalParseProcessingState = slotsService['parseProcessingState'];  | 
 | 195 | +		slotsService['parseProcessingState'] = () => mockProcessingState;  | 
 | 196 | +		  | 
 | 197 | +		// Trigger the processing state callbacks manually  | 
 | 198 | +		slotsService['callbacks'].forEach(callback => {  | 
 | 199 | +			try {  | 
 | 200 | +				callback(mockProcessingState);  | 
 | 201 | +			} catch (error) {  | 
 | 202 | +				console.error('Error in slots callback:', error);  | 
 | 203 | +			}  | 
 | 204 | +		});  | 
 | 205 | +		  | 
 | 206 | +		// Restore original method  | 
 | 207 | +		slotsService['parseProcessingState'] = originalParseProcessingState;  | 
100 | 208 | 	}}  | 
101 | 209 | />  | 
0 commit comments