Add API layer, Zustand store, markdown renderer, types
This commit is contained in:
+121
@@ -0,0 +1,121 @@
|
|||||||
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://api.aaronnelson.studio';
|
||||||
|
|
||||||
|
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||||
|
const res = await fetch(`${API_BASE}${path}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
// Settings
|
||||||
|
getSettings: () => request<Settings>('/api/settings'),
|
||||||
|
updateSettings: (s: Partial<Settings>) =>
|
||||||
|
request<Settings>('/api/settings', { method: 'POST', body: JSON.stringify(s) }),
|
||||||
|
|
||||||
|
// Conversations
|
||||||
|
getConversations: () => request<Conversation[]>('/api/conversations'),
|
||||||
|
newConversation: (title = 'New conversation') =>
|
||||||
|
request<Conversation>('/api/conversations', { method: 'POST', body: JSON.stringify({ title }) }),
|
||||||
|
getMessages: (id: string) => request<Message[]>(`/api/conversations/${id}/messages`),
|
||||||
|
renameConversation: (id: string, title: string) =>
|
||||||
|
request<Conversation>(`/api/conversations/${id}`, { method: 'PATCH', body: JSON.stringify({ title }) }),
|
||||||
|
deleteConversation: (id: string) =>
|
||||||
|
request<{ deleted: string }>(`/api/conversations/${id}`, { method: 'DELETE' }),
|
||||||
|
clearAllConversations: () =>
|
||||||
|
request<{ cleared: boolean }>('/api/conversations', { method: 'DELETE' }),
|
||||||
|
|
||||||
|
// Chat
|
||||||
|
sendMessage: (message: string, conversation_id: string) =>
|
||||||
|
request<ChatResponse>('/api/chat', { method: 'POST', body: JSON.stringify({ message, conversation_id }) }),
|
||||||
|
|
||||||
|
// Memory
|
||||||
|
getMemory: () => request<{ content: string }>('/api/memory'),
|
||||||
|
updateMemory: (content: string) =>
|
||||||
|
request<{ saved: boolean }>('/api/memory', { method: 'POST', body: JSON.stringify({ content }) }),
|
||||||
|
|
||||||
|
// Status
|
||||||
|
getStatus: () => request<Status>('/api/status'),
|
||||||
|
|
||||||
|
// Reindex
|
||||||
|
reindex: () => request<{ started: boolean }>('/api/reindex', { method: 'POST' }),
|
||||||
|
|
||||||
|
// Transcribe
|
||||||
|
transcribe: async (audio: Blob): Promise<{ text: string }> => {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('audio', audio, 'recording.webm');
|
||||||
|
const res = await fetch(`${API_BASE}/api/transcribe`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`Transcribe error: ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Capture
|
||||||
|
capture: async (data: CapturePayload): Promise<{ saved: boolean }> => {
|
||||||
|
const form = new FormData();
|
||||||
|
if (data.audio) form.append('audio', data.audio, 'capture.webm');
|
||||||
|
if (data.image) form.append('image', data.image);
|
||||||
|
if (data.text) form.append('text', data.text);
|
||||||
|
if (data.project) form.append('project', data.project);
|
||||||
|
const res = await fetch(`${API_BASE}/api/capture`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`Capture error: ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export interface Settings {
|
||||||
|
theme: 'light' | 'dark';
|
||||||
|
font_size: 'small' | 'medium' | 'large';
|
||||||
|
web_search: boolean;
|
||||||
|
show_sources: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Conversation {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
message_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
role: 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
sources: string[];
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatResponse {
|
||||||
|
response: string;
|
||||||
|
sources: string[];
|
||||||
|
conversation_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Status {
|
||||||
|
aaron_ai: string;
|
||||||
|
watcher: string;
|
||||||
|
chunk_count: number;
|
||||||
|
file_count: number;
|
||||||
|
last_indexed: string;
|
||||||
|
conversation_count: number;
|
||||||
|
model: string;
|
||||||
|
nextcloud_path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CapturePayload {
|
||||||
|
audio?: Blob;
|
||||||
|
image?: File;
|
||||||
|
text?: string;
|
||||||
|
project?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
marked.setOptions({
|
||||||
|
breaks: true,
|
||||||
|
gfm: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function renderMarkdown(text: string): string {
|
||||||
|
if (!text) return '';
|
||||||
|
return marked.parse(text) as string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
import type { Conversation, Message, Settings, Status } from './api';
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
// Conversations
|
||||||
|
conversations: Conversation[];
|
||||||
|
currentId: string | null;
|
||||||
|
messages: Message[];
|
||||||
|
setConversations: (c: Conversation[]) => void;
|
||||||
|
setCurrentId: (id: string | null) => void;
|
||||||
|
setMessages: (m: Message[]) => void;
|
||||||
|
addMessage: (m: Message) => void;
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
settings: Settings;
|
||||||
|
setSettings: (s: Partial<Settings>) => void;
|
||||||
|
|
||||||
|
// Status
|
||||||
|
status: Status | null;
|
||||||
|
setStatus: (s: Status) => void;
|
||||||
|
|
||||||
|
// UI
|
||||||
|
settingsOpen: boolean;
|
||||||
|
setSettingsOpen: (open: boolean) => void;
|
||||||
|
sidebarOpen: boolean;
|
||||||
|
setSidebarOpen: (open: boolean) => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
setIsLoading: (loading: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStore = create<AppState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
// Conversations
|
||||||
|
conversations: [],
|
||||||
|
currentId: null,
|
||||||
|
messages: [],
|
||||||
|
setConversations: (conversations) => set({ conversations }),
|
||||||
|
setCurrentId: (currentId) => set({ currentId }),
|
||||||
|
setMessages: (messages) => set({ messages }),
|
||||||
|
addMessage: (m) => set((s) => ({ messages: [...s.messages, m] })),
|
||||||
|
|
||||||
|
// Settings — defaults
|
||||||
|
settings: {
|
||||||
|
theme: 'light',
|
||||||
|
font_size: 'medium',
|
||||||
|
web_search: true,
|
||||||
|
show_sources: true,
|
||||||
|
},
|
||||||
|
setSettings: (s) =>
|
||||||
|
set((state) => ({ settings: { ...state.settings, ...s } })),
|
||||||
|
|
||||||
|
// Status
|
||||||
|
status: null,
|
||||||
|
setStatus: (status) => set({ status }),
|
||||||
|
|
||||||
|
// UI
|
||||||
|
settingsOpen: false,
|
||||||
|
setSettingsOpen: (settingsOpen) => set({ settingsOpen }),
|
||||||
|
sidebarOpen: false,
|
||||||
|
setSidebarOpen: (sidebarOpen) => set({ sidebarOpen }),
|
||||||
|
isLoading: false,
|
||||||
|
setIsLoading: (isLoading) => set({ isLoading }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'aaronai-store',
|
||||||
|
partialize: (state) => ({
|
||||||
|
settings: state.settings,
|
||||||
|
currentId: state.currentId,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export type Theme = 'light' | 'dark';
|
||||||
|
export type FontSize = 'small' | 'medium' | 'large';
|
||||||
|
export type DreamMode = 'nrem' | 'early-rem' | 'late-rem' | 'lucid';
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user