@@ -169,11 +169,302 @@ export const useAppStore = create<AppState>((set, get) => ({
169169 } ) ) ;
170170 } ,
171171
172+ // UI State
173+ isLoading : false ,
174+ isStreaming : false ,
175+ error : string | null ;
176+ setLoading : ( loading : boolean ) => void ;
177+ setStreaming : ( streaming : boolean ) => void ;
178+ setError: ( error : string | null ) => void ;
179+
180+ // Inference
181+ sendInferenceRequest: ( messages : Message [ ] ) => Promise < Message > ;
182+ sendInferenceRequestStream: ( messages : Message [ ] , onToken : ( token : string ) => void ) => Promise < void > ;
183+ }
184+
185+ export const useAppStore = create < AppState > ( ( set , get ) => ( {
186+ // Models
187+ models : [ ] ,
188+ selectedModel : null ,
189+ error : null ,
190+
191+ fetchModels : async ( ) => {
192+ try {
193+ const response = await fetch ( `${ API_BASE_URL } /v1/models` ) ;
194+ if ( ! response . ok ) throw new Error ( 'Failed to fetch models' ) ;
195+ const data = await response . json ( ) ;
196+
197+ const models : Model [ ] = data . data . map ( ( m : any ) => ( {
198+ id : m . id ,
199+ name : m . name || m . id ,
200+ description : `${ m . parameters || 'Unknown' } parameters` ,
201+ parameters : m . parameters || 0 ,
202+ quantization : m . quantization || 'unknown' ,
203+ size : `${ m . size_gb || 'Unknown' } GB` ,
204+ status : m . loaded ? 'loaded' as const : 'unloaded' as const ,
205+ } ) ) ;
206+
207+ set ( { models, error : null } ) ;
208+ } catch ( error ) {
209+ console . error ( 'Error fetching models:' , error ) ;
210+ set ( {
211+ error : 'Failed to connect to server. Make sure Mohawk server is running.' ,
212+ models : [
213+ { id : 'llama-3.2-3b-instruct' , name : 'Llama 3.2 3B Instruct' , description : '3B parameters, Q4_K_M' , parameters : 3000000000 , quantization : 'Q4_K_M' , size : '2.1 GB' , status : 'unloaded' as const } ,
214+ { id : 'mistral-7b-v0.3' , name : 'Mistral 7B v0.3' , description : '7B parameters, Q5_K_M' , parameters : 7000000000 , quantization : 'Q5_K_M' , size : '4.5 GB' , status : 'unloaded' as const } ,
215+ { id : 'phi-3-mini' , name : 'Phi-3 Mini' , description : '3.8B parameters, Q4_K_M' , parameters : 3800000000 , quantization : 'Q4_K_M' , size : '2.4 GB' , status : 'unloaded' as const } ,
216+ ]
217+ } ) ;
218+ }
219+ } ,
220+
221+ setModels : ( models ) => set ( { models } ) ,
222+ setSelectedModel : ( model ) => set ( { selectedModel : model } ) ,
223+
224+ loadModel : async ( modelId ) => {
225+ set ( { isLoading : true , error : null } ) ;
226+ try {
227+ const response = await fetch ( `${ API_BASE_URL } /v1/models/${ modelId } /load` , {
228+ method : 'POST' ,
229+ headers : { 'Content-Type' : 'application/json' } ,
230+ } ) ;
231+
232+ if ( ! response . ok ) {
233+ const errData = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
234+ throw new Error ( errData . error || 'Failed to load model' ) ;
235+ }
236+
237+ set ( ( state ) => ( {
238+ models : state . models . map ( m =>
239+ m . id === modelId ? { ...m , status : 'loaded' as const } : m
240+ ) ,
241+ selectedModel : state . models . find ( m => m . id === modelId ) || null ,
242+ isLoading : false ,
243+ } ) ) ;
244+ } catch ( error ) {
245+ set ( {
246+ isLoading : false ,
247+ error : error instanceof Error ? error . message : 'Failed to load model'
248+ } ) ;
249+ throw error ;
250+ }
251+ } ,
252+
253+ unloadModel : async ( modelId ) => {
254+ set ( { isLoading : true , error : null } ) ;
255+ try {
256+ const response = await fetch ( `${ API_BASE_URL } /v1/models/${ modelId } /unload` , {
257+ method : 'POST' ,
258+ headers : { 'Content-Type' : 'application/json' } ,
259+ } ) ;
260+
261+ if ( ! response . ok ) throw new Error ( 'Failed to unload model' ) ;
262+
263+ set ( ( state ) => ( {
264+ models : state . models . map ( m =>
265+ m . id === modelId ? { ...m , status : 'unloaded' as const } : m
266+ ) ,
267+ selectedModel : state . selectedModel ?. id === modelId ? null : state . selectedModel ,
268+ isLoading : false ,
269+ } ) ) ;
270+ } catch ( error ) {
271+ set ( {
272+ isLoading : false ,
273+ error : error instanceof Error ? error . message : 'Failed to unload model'
274+ } ) ;
275+ throw error ;
276+ }
277+ } ,
278+
279+ // Chat Sessions
280+ sessions : [ ] ,
281+ currentSession : null ,
282+ setSessions : ( sessions ) => set ( { sessions } ) ,
283+ setCurrentSession : ( session ) => set ( { currentSession : session } ) ,
284+ createSession : ( ) => {
285+ const newSession : ChatSession = {
286+ id : crypto . randomUUID ( ) ,
287+ title : 'New Chat' ,
288+ messages : [ ] ,
289+ modelId : get ( ) . selectedModel ?. id || '' ,
290+ createdAt : new Date ( ) ,
291+ updatedAt : new Date ( ) ,
292+ } ;
293+ set ( ( state ) => ( {
294+ sessions : [ ...state . sessions , newSession ] ,
295+ currentSession : newSession ,
296+ } ) ) ;
297+ return newSession ;
298+ } ,
299+ deleteSession : ( sessionId ) => {
300+ set ( ( state ) => ( {
301+ sessions : state . sessions . filter ( s => s . id !== sessionId ) ,
302+ currentSession : state . currentSession ?. id === sessionId ? null : state . currentSession ,
303+ } ) ) ;
304+ } ,
305+ addMessage : ( sessionId , message ) => {
306+ set ( ( state ) => ( {
307+ sessions : state . sessions . map ( s =>
308+ s . id === sessionId
309+ ? { ...s , messages : [ ...s . messages , message ] , updatedAt : new Date ( ) }
310+ : s
311+ ) ,
312+ currentSession : state . currentSession ?. id === sessionId
313+ ? { ...state . currentSession , messages : [ ...state . currentSession . messages , message ] , updatedAt : new Date ( ) }
314+ : state . currentSession ,
315+ } ) ) ;
316+ } ,
317+
318+ // Settings
319+ settings : {
320+ temperature : 0.7 ,
321+ topP : 0.9 ,
322+ topK : 40 ,
323+ maxTokens : 2048 ,
324+ stopSequences : [ ] ,
325+ systemPrompt : 'You are a helpful AI assistant.' ,
326+ } ,
327+ updateSettings : ( newSettings ) => {
328+ set ( ( state ) => ( {
329+ settings : { ...state . settings , ...newSettings } ,
330+ } ) ) ;
331+ } ,
332+
172333 // UI State
173334 isLoading : false ,
174335 isStreaming : false ,
175336 error : null ,
176337 setLoading : ( loading ) => set ( { isLoading : loading } ) ,
177338 setStreaming : ( streaming ) => set ( { isStreaming : streaming } ) ,
178339 setError : ( error ) => set ( { error } ) ,
340+
341+ // Inference - Non-streaming
342+ sendInferenceRequest : async ( messages : Message [ ] ) => {
343+ const state = get ( ) ;
344+ const { selectedModel, settings } = state ;
345+
346+ if ( ! selectedModel ) {
347+ throw new Error ( 'No model selected' ) ;
348+ }
349+
350+ set ( { isLoading : true , error : null } ) ;
351+
352+ try {
353+ const response = await fetch ( `${ API_BASE_URL } /v1/chat/completions` , {
354+ method : 'POST' ,
355+ headers : { 'Content-Type' : 'application/json' } ,
356+ body : JSON . stringify ( {
357+ model : selectedModel . id ,
358+ messages : messages . map ( m => ( { role : m . role , content : m . content } ) ) ,
359+ temperature : settings . temperature ,
360+ top_p : settings . topP ,
361+ top_k : settings . topK ,
362+ max_tokens : settings . maxTokens ,
363+ stream : false ,
364+ } ) ,
365+ } ) ;
366+
367+ if ( ! response . ok ) {
368+ const errData = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
369+ throw new Error ( errData . error || 'Inference request failed' ) ;
370+ }
371+
372+ const data = await response . json ( ) ;
373+ const choice = data . choices [ 0 ] ;
374+
375+ const assistantMessage : Message = {
376+ id : crypto . randomUUID ( ) ,
377+ role : 'assistant' ,
378+ content : choice . message . content ,
379+ timestamp : new Date ( ) ,
380+ } ;
381+
382+ set ( { isLoading : false } ) ;
383+ return assistantMessage ;
384+ } catch ( error ) {
385+ set ( {
386+ isLoading : false ,
387+ error : error instanceof Error ? error . message : 'Inference request failed'
388+ } ) ;
389+ throw error ;
390+ }
391+ } ,
392+
393+ // Inference - Streaming
394+ sendInferenceRequestStream : async ( messages : Message [ ] , onToken : ( token : string ) => void ) => {
395+ const state = get ( ) ;
396+ const { selectedModel, settings } = state ;
397+
398+ if ( ! selectedModel ) {
399+ throw new Error ( 'No model selected' ) ;
400+ }
401+
402+ set ( { isStreaming : true , error : null } ) ;
403+
404+ try {
405+ const response = await fetch ( `${ API_BASE_URL } /v1/chat/completions` , {
406+ method : 'POST' ,
407+ headers : { 'Content-Type' : 'application/json' } ,
408+ body : JSON . stringify ( {
409+ model : selectedModel . id ,
410+ messages : messages . map ( m => ( { role : m . role , content : m . content } ) ) ,
411+ temperature : settings . temperature ,
412+ top_p : settings . topP ,
413+ top_k : settings . topK ,
414+ max_tokens : settings . maxTokens ,
415+ stream : true ,
416+ } ) ,
417+ } ) ;
418+
419+ if ( ! response . ok ) {
420+ const errData = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
421+ throw new Error ( errData . error || 'Streaming request failed' ) ;
422+ }
423+
424+ const reader = response . body ?. getReader ( ) ;
425+ if ( ! reader ) {
426+ throw new Error ( 'No response body' ) ;
427+ }
428+
429+ const decoder = new TextDecoder ( ) ;
430+ let buffer = '' ;
431+
432+ while ( true ) {
433+ const { done, value } = await reader . read ( ) ;
434+ if ( done ) break ;
435+
436+ buffer += decoder . decode ( value , { stream : true } ) ;
437+ const lines = buffer . split ( '\n' ) ;
438+ buffer = lines . pop ( ) || '' ;
439+
440+ for ( const line of lines ) {
441+ if ( line . startsWith ( 'data: ' ) ) {
442+ const data = line . slice ( 6 ) ;
443+ if ( data === '[DONE]' ) {
444+ set ( { isStreaming : false } ) ;
445+ return ;
446+ }
447+ try {
448+ const parsed = JSON . parse ( data ) ;
449+ const choice = parsed . choices [ 0 ] ;
450+ const token = choice . delta ?. content || '' ;
451+ if ( token ) {
452+ onToken ( token ) ;
453+ }
454+ } catch ( e ) {
455+ console . error ( 'Failed to parse SSE data:' , e ) ;
456+ }
457+ }
458+ }
459+ }
460+
461+ set ( { isStreaming : false } ) ;
462+ } catch ( error ) {
463+ set ( {
464+ isStreaming : false ,
465+ error : error instanceof Error ? error . message : 'Streaming request failed'
466+ } ) ;
467+ throw error ;
468+ }
469+ } ,
179470} ) ) ;
0 commit comments