Skip to content

Commit db6679d

Browse files
Merge pull request #13 from rwilliamspbg-ops/finalizing-mohawk-inference-engine-7ad4f
Update from task 845a67bb-4b33-47b0-8d2b-84b42117ad4f
2 parents 1cde208 + e8569a2 commit db6679d

1 file changed

Lines changed: 291 additions & 0 deletions

File tree

gui/src/store.ts

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)