diff --git a/lib/api.ts b/lib/api.ts new file mode 100644 index 0000000..1cae2dd --- /dev/null +++ b/lib/api.ts @@ -0,0 +1,121 @@ +const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://api.aaronnelson.studio'; + +async function request(path: string, options?: RequestInit): Promise { + 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('/api/settings'), + updateSettings: (s: Partial) => + request('/api/settings', { method: 'POST', body: JSON.stringify(s) }), + + // Conversations + getConversations: () => request('/api/conversations'), + newConversation: (title = 'New conversation') => + request('/api/conversations', { method: 'POST', body: JSON.stringify({ title }) }), + getMessages: (id: string) => request(`/api/conversations/${id}/messages`), + renameConversation: (id: string, title: string) => + request(`/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('/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('/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; +} diff --git a/lib/markdown.ts b/lib/markdown.ts new file mode 100644 index 0000000..8c256e8 --- /dev/null +++ b/lib/markdown.ts @@ -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; +} diff --git a/lib/store.ts b/lib/store.ts new file mode 100644 index 0000000..c8a13fd --- /dev/null +++ b/lib/store.ts @@ -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) => 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()( + 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, + }), + } + ) +); diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 0000000..283670c --- /dev/null +++ b/types/index.ts @@ -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; +}