From a08db90eb3f45a0bca475e3473b651b06e5a505c Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 27 Apr 2026 16:49:17 +0100 Subject: [PATCH 1/2] Fix chat persistence fallback and harden admin analytics queries --- app/actions/generate.ts | 55 ++++++++++++--- app/api/admin/analytics/route.ts | 69 +++++++++++++------ ...add_chat_sessions_session_id_and_title.sql | 20 ++++++ ...add_chat_sessions_session_id_and_title.sql | 21 ++++++ 4 files changed, 133 insertions(+), 32 deletions(-) create mode 100644 migrations/add_chat_sessions_session_id_and_title.sql create mode 100644 supabase/migrations/20260427110000_add_chat_sessions_session_id_and_title.sql diff --git a/app/actions/generate.ts b/app/actions/generate.ts index bc26957..d715dba 100644 --- a/app/actions/generate.ts +++ b/app/actions/generate.ts @@ -40,6 +40,11 @@ function isMissingColumnError(error: unknown, columnName: string) { return details.includes(columnName.toLowerCase()) && details.includes('column') } +function omitField, K extends keyof T>(payload: T, key: K): Omit { + const { [key]: _removed, ...rest } = payload + return rest +} + export async function generateAnswer({ prompt, tool, authorId, authorEmail, attachments = [], sessionId, chatId, enableWebSearch = false, researchMode = false }: GenerateProps) { // Get user profile and check limits let userProfile = await getUserProfileServer(authorId) @@ -234,24 +239,50 @@ export async function generateAnswer({ prompt, tool, authorId, authorEmail, atta title: title } - let { data, error } = await supabaseServer.from('chat_sessions').insert({ + let insertPayload: Record = { ...baseInsertPayload, token_usage: tokenCost, - }) - .select('id') - .single() + } + let data: { id: string } | null = null + let error: any = null - if (error && isMissingColumnError(error, 'token_usage')) { - const retryResult = await supabaseServer.from('chat_sessions').insert(baseInsertPayload) + for (let attempt = 0; attempt < 4; attempt += 1) { + const result = await supabaseServer + .from('chat_sessions') + .insert(insertPayload) .select('id') .single() - data = retryResult.data - error = retryResult.error + data = result.data + error = result.error + + if (!error) break + + if (isMissingColumnError(error, 'token_usage') && 'token_usage' in insertPayload) { + insertPayload = omitField(insertPayload, 'token_usage') + continue + } + + if (isMissingColumnError(error, 'session_id') && 'session_id' in insertPayload) { + insertPayload = omitField(insertPayload, 'session_id') + continue + } + + if (isMissingColumnError(error, 'title') && 'title' in insertPayload) { + insertPayload = omitField(insertPayload, 'title') + continue + } + + break } if (error) { - console.error('[chat_insert_failed]', { userId: authorId, sessionId: currentSessionId, error }) + console.error('[chat_insert_failed]', { + userId: authorId, + sessionId: currentSessionId, + error, + attemptedPayloadKeys: Object.keys(insertPayload) + }) persistenceWarning = 'We generated your response, but could not save this chat message.' } else if (data?.id) { savedChatId = data.id @@ -259,8 +290,10 @@ export async function generateAnswer({ prompt, tool, authorId, authorEmail, atta } } - // Increment chat counter after successful generation - await incrementChatsServer(authorId) + // Increment chat counter only after successful persistence to avoid analytics drift. + if (chatPersisted) { + await incrementChatsServer(authorId) + } // Increment web search counter if enabled if (enableWebSearch) { diff --git a/app/api/admin/analytics/route.ts b/app/api/admin/analytics/route.ts index 5501c91..810d208 100644 --- a/app/api/admin/analytics/route.ts +++ b/app/api/admin/analytics/route.ts @@ -3,6 +3,12 @@ import { auth } from '@/lib/auth' import { supabaseServer as supabase } from '@/lib/supabase-server' import { isAdminUser } from '@/lib/admin' +function throwIfSupabaseError(error: any, context: string) { + if (error) { + throw new Error(`[admin-analytics:${context}] ${error.message || 'Supabase query failed'}`) + } +} + export async function POST(req: NextRequest) { try { const session = await auth() @@ -33,44 +39,51 @@ async function getAnalyticsData() { // ===== USER METRICS ===== // Total users - const { count: totalUsers } = await supabase + const { count: totalUsers, error: totalUsersError } = await supabase .from('users') .select('*', { count: 'exact', head: true }) + throwIfSupabaseError(totalUsersError, 'total-users') // New users today - const { count: newUsersToday } = await supabase + const { count: newUsersToday, error: newUsersTodayError } = await supabase .from('users') .select('*', { count: 'exact', head: true }) .gte('created_at', todayStart) + throwIfSupabaseError(newUsersTodayError, 'new-users-today') // New users this week - const { count: newUsersWeek } = await supabase + const { count: newUsersWeek, error: newUsersWeekError } = await supabase .from('users') .select('*', { count: 'exact', head: true }) .gte('created_at', sevenDaysAgo) + throwIfSupabaseError(newUsersWeekError, 'new-users-week') // New users this month - const { count: newUsersMonth } = await supabase + const { count: newUsersMonth, error: newUsersMonthError } = await supabase .from('users') .select('*', { count: 'exact', head: true }) .gte('created_at', thirtyDaysAgo) + throwIfSupabaseError(newUsersMonthError, 'new-users-month') // Users who hit chat limit - const { count: chatLimitHits } = await supabase + const { count: chatLimitHits, error: chatLimitHitsError } = await supabase .from('users') .select('*', { count: 'exact', head: true }) .not('limit_hit_chat_at', 'is', null) + throwIfSupabaseError(chatLimitHitsError, 'chat-limit-hits') // Users who hit upload limit - const { count: uploadLimitHits } = await supabase + const { count: uploadLimitHits, error: uploadLimitHitsError } = await supabase .from('users') .select('*', { count: 'exact', head: true }) .not('limit_hit_upload_at', 'is', null) + throwIfSupabaseError(uploadLimitHitsError, 'upload-limit-hits') // Subscription breakdown - const { data: subscriptionBreakdown } = await supabase + const { data: subscriptionBreakdown, error: subscriptionBreakdownError } = await supabase .from('users') .select('subscription_plan') + throwIfSupabaseError(subscriptionBreakdownError, 'subscription-breakdown') const plans = { free: 0, pro: 0, plus: 0, school: 0 } subscriptionBreakdown?.forEach((user: any) => { @@ -82,41 +95,47 @@ async function getAnalyticsData() { // ===== CHAT SESSION METRICS ===== // Total chat sessions - const { count: totalChatSessions } = await supabase + const { count: totalChatSessions, error: totalChatSessionsError } = await supabase .from('chat_sessions') .select('*', { count: 'exact', head: true }) + throwIfSupabaseError(totalChatSessionsError, 'total-chat-sessions') // Chat sessions today - const { count: chatsToday } = await supabase + const { count: chatsToday, error: chatsTodayError } = await supabase .from('chat_sessions') .select('*', { count: 'exact', head: true }) .gte('created_at', todayStart) + throwIfSupabaseError(chatsTodayError, 'chats-today') // Chat sessions this week - const { count: chatsThisWeek } = await supabase + const { count: chatsThisWeek, error: chatsThisWeekError } = await supabase .from('chat_sessions') .select('*', { count: 'exact', head: true }) .gte('created_at', sevenDaysAgo) + throwIfSupabaseError(chatsThisWeekError, 'chats-this-week') // Active users today (users who had chats today) - const { data: activeUsersData } = await supabase + const { data: activeUsersData, error: activeUsersDataError } = await supabase .from('chat_sessions') .select('user_id') .gte('created_at', todayStart) + throwIfSupabaseError(activeUsersDataError, 'active-users-today') const activeUsersToday = new Set(activeUsersData?.map((c: any) => c.user_id) || []).size // Active users this week - const { data: weeklyActiveData } = await supabase + const { data: weeklyActiveData, error: weeklyActiveDataError } = await supabase .from('chat_sessions') .select('user_id') .gte('created_at', sevenDaysAgo) + throwIfSupabaseError(weeklyActiveDataError, 'active-users-week') const activeUsersWeek = new Set(weeklyActiveData?.map((c: any) => c.user_id) || []).size // ===== WEB SEARCH METRICS ===== // Total web searches (sum of monthly_web_searches column) - const { data: webSearchData } = await supabase + const { data: webSearchData, error: webSearchDataError } = await supabase .from('users') .select('monthly_web_searches') + throwIfSupabaseError(webSearchDataError, 'web-search-data') const totalWebSearches = webSearchData?.reduce((sum: number, u: any) => sum + (u.monthly_web_searches || 0), 0) || 0 // ===== DAILY ACTIVITY CHART DATA (Last 7 days) ===== @@ -126,17 +145,19 @@ async function getAnalyticsData() { dayStart.setHours(0, 0, 0, 0) const dayEnd = new Date(dayStart.getTime() + 24 * 60 * 60 * 1000) - const { count: dayChats } = await supabase + const { count: dayChats, error: dayChatsError } = await supabase .from('chat_sessions') .select('*', { count: 'exact', head: true }) .gte('created_at', dayStart.toISOString()) .lt('created_at', dayEnd.toISOString()) + throwIfSupabaseError(dayChatsError, `daily-chats-${i}`) - const { count: dayUsers } = await supabase + const { count: dayUsers, error: dayUsersError } = await supabase .from('users') .select('*', { count: 'exact', head: true }) .gte('created_at', dayStart.toISOString()) .lt('created_at', dayEnd.toISOString()) + throwIfSupabaseError(dayUsersError, `daily-users-${i}`) dailyActivity.push({ date: dayStart.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }), @@ -146,10 +167,11 @@ async function getAnalyticsData() { } // ===== TOP ACTIVE USERS ===== - const { data: topUsersData } = await supabase + const { data: topUsersData, error: topUsersDataError } = await supabase .from('chat_sessions') .select('user_id') .gte('created_at', sevenDaysAgo) + throwIfSupabaseError(topUsersDataError, 'top-users-data') const userChatCount: Record = {} topUsersData?.forEach((chat: any) => { @@ -164,10 +186,11 @@ async function getAnalyticsData() { .slice(0, 10) .map(([id]) => id) - const { data: topUsersInfo } = await supabase + const { data: topUsersInfo, error: topUsersInfoError } = await supabase .from('users') .select('id, email, subscription_plan, created_at') .in('id', topUserIds.length > 0 ? topUserIds : ['none']) + throwIfSupabaseError(topUsersInfoError, 'top-users-info') const topActiveUsers = topUsersInfo?.map((user: any) => ({ ...user, @@ -175,32 +198,36 @@ async function getAnalyticsData() { })).sort((a: any, b: any) => b.chatCount - a.chatCount) || [] // ===== RECENT SIGNUPS ===== - const { data: recentSignups } = await supabase + const { data: recentSignups, error: recentSignupsError } = await supabase .from('users') .select('id, email, subscription_plan, created_at') .order('created_at', { ascending: false }) .limit(10) + throwIfSupabaseError(recentSignupsError, 'recent-signups') // Users who upgraded after hitting limit - const { data: upgradedAfterLimit } = await supabase + const { data: upgradedAfterLimit, error: upgradedAfterLimitError } = await supabase .from('users') .select('id, email, subscription_plan, limit_hit_chat_at, limit_hit_upload_at, created_at') .neq('subscription_plan', 'free') .or('limit_hit_chat_at.not.is.null, limit_hit_upload_at.not.is.null') + throwIfSupabaseError(upgradedAfterLimitError, 'upgraded-after-limit') // Users still locked out - const { data: lockedOutUsers } = await supabase + const { data: lockedOutUsers, error: lockedOutUsersError } = await supabase .from('users') .select('id, email, subscription_plan, limit_hit_chat_at, limit_hit_upload_at') .or(`limit_hit_chat_at.gt.${oneDayAgo}, limit_hit_upload_at.gt.${oneDayAgo}`) + throwIfSupabaseError(lockedOutUsersError, 'locked-out-users') // Recent limit hits (last 7 days) - const { data: recentLimitHits } = await supabase + const { data: recentLimitHits, error: recentLimitHitsError } = await supabase .from('users') .select('id, email, subscription_plan, limit_hit_chat_at, limit_hit_upload_at, created_at') .or(`limit_hit_chat_at.gte.${sevenDaysAgo}, limit_hit_upload_at.gte.${sevenDaysAgo}`) .order('created_at', { ascending: false }) .limit(50) + throwIfSupabaseError(recentLimitHitsError, 'recent-limit-hits') // Upgrade rate calculation const upgradedCount = (upgradedAfterLimit || []).length diff --git a/migrations/add_chat_sessions_session_id_and_title.sql b/migrations/add_chat_sessions_session_id_and_title.sql new file mode 100644 index 0000000..9ba2bd9 --- /dev/null +++ b/migrations/add_chat_sessions_session_id_and_title.sql @@ -0,0 +1,20 @@ +-- Ensure chat_sessions has session_id/title required by server actions and admin analytics. +-- Keep this copy aligned with supabase/migrations for environments using the root migrations folder. + +ALTER TABLE chat_sessions +ADD COLUMN IF NOT EXISTS session_id UUID, +ADD COLUMN IF NOT EXISTS title TEXT; + +UPDATE chat_sessions +SET session_id = gen_random_uuid() +WHERE session_id IS NULL; + +ALTER TABLE chat_sessions +ALTER COLUMN session_id SET DEFAULT gen_random_uuid(), +ALTER COLUMN session_id SET NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_chat_sessions_session_id +ON chat_sessions (session_id); + +CREATE INDEX IF NOT EXISTS idx_chat_sessions_user_session_created_at +ON chat_sessions (user_id, session_id, created_at DESC); diff --git a/supabase/migrations/20260427110000_add_chat_sessions_session_id_and_title.sql b/supabase/migrations/20260427110000_add_chat_sessions_session_id_and_title.sql new file mode 100644 index 0000000..7300def --- /dev/null +++ b/supabase/migrations/20260427110000_add_chat_sessions_session_id_and_title.sql @@ -0,0 +1,21 @@ +-- Ensure chat_sessions has session_id/title required by server actions and admin analytics. +-- This migration is idempotent and safe for environments that already applied these columns. + +ALTER TABLE chat_sessions +ADD COLUMN IF NOT EXISTS session_id UUID, +ADD COLUMN IF NOT EXISTS title TEXT; + +-- Backfill old rows so downstream queries and grouping by session_id remain reliable. +UPDATE chat_sessions +SET session_id = gen_random_uuid() +WHERE session_id IS NULL; + +ALTER TABLE chat_sessions +ALTER COLUMN session_id SET DEFAULT gen_random_uuid(), +ALTER COLUMN session_id SET NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_chat_sessions_session_id +ON chat_sessions (session_id); + +CREATE INDEX IF NOT EXISTS idx_chat_sessions_user_session_created_at +ON chat_sessions (user_id, session_id, created_at DESC); From adbb57ca8288a1ebb05aec0690a9deeb566e7812 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 27 Apr 2026 17:07:05 +0100 Subject: [PATCH 2/2] Analytics issue --- ...c660-4f68-b35d-4d71d6b82460_1776695663.png | Bin 0 -> 14919 bytes ...7321-4c5d-9b35-1a01dc5c0c86_1776692926.png | Bin 0 -> 14919 bytes .../18bdd766-de87-4aea-a95e-65b08636d21a.log | 3 ++ .../1d3f6f74-62a1-4546-9546-440bc9eec176.log | 4 ++ .../23c8bec4-f82e-4857-a6de-67f3b8a60eec.log | 3 ++ .../c68159c7-9e6e-4271-90b6-73a6414bfba0.log | 3 ++ .forge/executions/state.json | 7 +++ .../2d054b74-476c-4350-9b0d-01fe30d3dea3.json | 42 ++++++++++++++++++ .../d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff.json | 42 ++++++++++++++++++ .../e44ed371-65d0-4399-97a1-fd2bbf46de00.json | 42 ++++++++++++++++++ .forge/provider_config.json | 6 +++ .forge/provider_secret.key | 1 + mobile/metro.config.js | 7 +++ mobile/package.json | 38 +--------------- 14 files changed, 161 insertions(+), 37 deletions(-) create mode 100644 .forge/browser/artifacts/screenshot_697e344c-c660-4f68-b35d-4d71d6b82460_1776695663.png create mode 100644 .forge/browser/artifacts/screenshot_6ded4b1d-7321-4c5d-9b35-1a01dc5c0c86_1776692926.png create mode 100644 .forge/executions/18bdd766-de87-4aea-a95e-65b08636d21a.log create mode 100644 .forge/executions/1d3f6f74-62a1-4546-9546-440bc9eec176.log create mode 100644 .forge/executions/23c8bec4-f82e-4857-a6de-67f3b8a60eec.log create mode 100644 .forge/executions/c68159c7-9e6e-4271-90b6-73a6414bfba0.log create mode 100644 .forge/executions/state.json create mode 100644 .forge/plans/2d054b74-476c-4350-9b0d-01fe30d3dea3.json create mode 100644 .forge/plans/d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff.json create mode 100644 .forge/plans/e44ed371-65d0-4399-97a1-fd2bbf46de00.json create mode 100644 .forge/provider_config.json create mode 100644 .forge/provider_secret.key create mode 100644 mobile/metro.config.js diff --git a/.forge/browser/artifacts/screenshot_697e344c-c660-4f68-b35d-4d71d6b82460_1776695663.png b/.forge/browser/artifacts/screenshot_697e344c-c660-4f68-b35d-4d71d6b82460_1776695663.png new file mode 100644 index 0000000000000000000000000000000000000000..c1720eb0196458ddc7aa33923bec15fb96754d6b GIT binary patch literal 14919 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 M85L>x>-S&&8!@R9OaK4? literal 0 HcmV?d00001 diff --git a/.forge/browser/artifacts/screenshot_6ded4b1d-7321-4c5d-9b35-1a01dc5c0c86_1776692926.png b/.forge/browser/artifacts/screenshot_6ded4b1d-7321-4c5d-9b35-1a01dc5c0c86_1776692926.png new file mode 100644 index 0000000000000000000000000000000000000000..c1720eb0196458ddc7aa33923bec15fb96754d6b GIT binary patch literal 14919 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 M85L>x>-S&&8!@R9OaK4? literal 0 HcmV?d00001 diff --git a/.forge/executions/18bdd766-de87-4aea-a95e-65b08636d21a.log b/.forge/executions/18bdd766-de87-4aea-a95e-65b08636d21a.log new file mode 100644 index 0000000..fff9530 --- /dev/null +++ b/.forge/executions/18bdd766-de87-4aea-a95e-65b08636d21a.log @@ -0,0 +1,3 @@ +[2026-04-20T13:45:28.457860900+00:00] Started execution for plan: 2d054b74-476c-4350-9b0d-01fe30d3dea3 +[2026-04-20T13:45:28.532783200+00:00] [Provider] Refining step: Analyze workspace context +[2026-04-20T13:45:29.666195600+00:00] Resolved action: InspectFiles against src diff --git a/.forge/executions/1d3f6f74-62a1-4546-9546-440bc9eec176.log b/.forge/executions/1d3f6f74-62a1-4546-9546-440bc9eec176.log new file mode 100644 index 0000000..8006e1f --- /dev/null +++ b/.forge/executions/1d3f6f74-62a1-4546-9546-440bc9eec176.log @@ -0,0 +1,4 @@ +[2026-04-20T14:01:25.320370100+00:00] Started execution for plan: d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff +[2026-04-20T14:01:25.392901100+00:00] [Provider] Refining step: Analyze workspace context +[2026-04-20T14:01:51.628467+00:00] [Provider] Refinement failed: Request failed: error sending request for url (https://api.mistral.ai/v1/chat/completions). Using fallback. +[2026-04-20T14:01:51.629282600+00:00] Resolved action: InspectFiles against diff --git a/.forge/executions/23c8bec4-f82e-4857-a6de-67f3b8a60eec.log b/.forge/executions/23c8bec4-f82e-4857-a6de-67f3b8a60eec.log new file mode 100644 index 0000000..7ccdad3 --- /dev/null +++ b/.forge/executions/23c8bec4-f82e-4857-a6de-67f3b8a60eec.log @@ -0,0 +1,3 @@ +[2026-04-20T13:44:37.566968200+00:00] Started execution for plan: 2d054b74-476c-4350-9b0d-01fe30d3dea3 +[2026-04-20T13:44:37.662824400+00:00] [Provider] Refining step: Analyze workspace context +[2026-04-20T13:44:39.095675800+00:00] Resolved action: InspectFiles against src diff --git a/.forge/executions/c68159c7-9e6e-4271-90b6-73a6414bfba0.log b/.forge/executions/c68159c7-9e6e-4271-90b6-73a6414bfba0.log new file mode 100644 index 0000000..29d939c --- /dev/null +++ b/.forge/executions/c68159c7-9e6e-4271-90b6-73a6414bfba0.log @@ -0,0 +1,3 @@ +[2026-04-20T14:33:38.557977700+00:00] Started execution for plan: d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff +[2026-04-20T14:33:38.637442100+00:00] [Provider] Refining step: Analyze workspace context +[2026-04-20T14:33:40.691084300+00:00] Resolved action: InspectFiles against src diff --git a/.forge/executions/state.json b/.forge/executions/state.json new file mode 100644 index 0000000..2e404dd --- /dev/null +++ b/.forge/executions/state.json @@ -0,0 +1,7 @@ +{ + "id": "c68159c7-9e6e-4271-90b6-73a6414bfba0", + "planId": "d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff", + "status": "running", + "mode": "step_by_step", + "currentStepId": "step_1" +} \ No newline at end of file diff --git a/.forge/plans/2d054b74-476c-4350-9b0d-01fe30d3dea3.json b/.forge/plans/2d054b74-476c-4350-9b0d-01fe30d3dea3.json new file mode 100644 index 0000000..e7eccad --- /dev/null +++ b/.forge/plans/2d054b74-476c-4350-9b0d-01fe30d3dea3.json @@ -0,0 +1,42 @@ +{ + "id": "2d054b74-476c-4350-9b0d-01fe30d3dea3", + "taskId": "7fbdc7ca-d44b-47ca-9db1-30ad5c19b87c", + "status": "approved", + "title": "Plan for: what is this project about?", + "objective": "what is this project about?", + "steps": [ + { + "id": "step_1", + "kind": "inspect", + "title": "Analyze workspace context", + "objective": "Assess current state of relevant files.", + "status": "pending", + "filesLikelyInvolved": [], + "requiredTools": [ + "fs_list" + ] + }, + { + "id": "step_2", + "kind": "edit", + "title": "Implement changes", + "objective": "```json\n{\n \"plan\": {\n \"id\": \"plan_1\",\n \"taskId\": \"task_1\",\n \"status\": \"draft\",\n \"title\": \"Analyze Tera Project\",\n \"objective\": \"Understand the purpose and structure of the Tera project", + "status": "pending", + "filesLikelyInvolved": [ + "src/main.rs" + ], + "requiredTools": [ + "fs_write" + ] + } + ], + "dependencies": [ + { + "stepId": "step_2", + "dependsOn": "step_1" + } + ], + "assumptions": [], + "risks": [], + "architectureProposal": null +} \ No newline at end of file diff --git a/.forge/plans/d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff.json b/.forge/plans/d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff.json new file mode 100644 index 0000000..d5b8ba7 --- /dev/null +++ b/.forge/plans/d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff.json @@ -0,0 +1,42 @@ +{ + "id": "d0c3c8e7-00bc-46e9-a4dc-650684f9a2ff", + "taskId": "2c368bff-7a21-47c5-8640-dc0f68bd74b1", + "status": "approved", + "title": "Plan for: tell me what this project is about", + "objective": "tell me what this project is about", + "steps": [ + { + "id": "step_1", + "kind": "inspect", + "title": "Analyze workspace context", + "objective": "Assess current state of relevant files.", + "status": "pending", + "filesLikelyInvolved": [], + "requiredTools": [ + "fs_list" + ] + }, + { + "id": "step_2", + "kind": "edit", + "title": "Implement changes", + "objective": "```json\n{\n \"plan\": {\n \"id\": \"3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d\",\n \"taskId\": \"project_analysis\",\n \"status\": \"draft\",\n \"title\": \"Analyze Tera Project\",\n \"objective\": \"Determine what t", + "status": "pending", + "filesLikelyInvolved": [ + "src/main.rs" + ], + "requiredTools": [ + "fs_write" + ] + } + ], + "dependencies": [ + { + "stepId": "step_2", + "dependsOn": "step_1" + } + ], + "assumptions": [], + "risks": [], + "architectureProposal": null +} \ No newline at end of file diff --git a/.forge/plans/e44ed371-65d0-4399-97a1-fd2bbf46de00.json b/.forge/plans/e44ed371-65d0-4399-97a1-fd2bbf46de00.json new file mode 100644 index 0000000..684c2b2 --- /dev/null +++ b/.forge/plans/e44ed371-65d0-4399-97a1-fd2bbf46de00.json @@ -0,0 +1,42 @@ +{ + "id": "e44ed371-65d0-4399-97a1-fd2bbf46de00", + "taskId": "5b12269e-62c9-41d1-af4d-a9d06bc811d5", + "status": "ready_for_review", + "title": "Plan for: what is Tera about", + "objective": "what is Tera about", + "steps": [ + { + "id": "step_1", + "kind": "inspect", + "title": "Analyze workspace context", + "objective": "Assess current state of relevant files.", + "status": "pending", + "filesLikelyInvolved": [], + "requiredTools": [ + "fs_list" + ] + }, + { + "id": "step_2", + "kind": "edit", + "title": "Implement changes", + "objective": "```json\n{\n \"plan\": {\n \"id\": \"c0f7b5e8-1234-5678-9abc-def123456789\",\n \"taskId\": \"what_is_tera_about\",\n \"status\": \"draft\",\n \"title\": \"Investigate Tera project to understand its purpose\",\n ", + "status": "pending", + "filesLikelyInvolved": [ + "src/main.rs" + ], + "requiredTools": [ + "fs_write" + ] + } + ], + "dependencies": [ + { + "stepId": "step_2", + "dependsOn": "step_1" + } + ], + "assumptions": [], + "risks": [], + "architectureProposal": null +} \ No newline at end of file diff --git a/.forge/provider_config.json b/.forge/provider_config.json new file mode 100644 index 0000000..370e891 --- /dev/null +++ b/.forge/provider_config.json @@ -0,0 +1,6 @@ +{ + "kind": "openai_compatible", + "baseUrl": "https://api.mistral.ai", + "modelId": "mistral-small-latest", + "apiKeySet": true +} \ No newline at end of file diff --git a/.forge/provider_secret.key b/.forge/provider_secret.key new file mode 100644 index 0000000..8959165 --- /dev/null +++ b/.forge/provider_secret.key @@ -0,0 +1 @@ +v1Vphvx1drTK9OdsQBv1lsTVr4bsaBrv \ No newline at end of file diff --git a/mobile/metro.config.js b/mobile/metro.config.js new file mode 100644 index 0000000..73ba303 --- /dev/null +++ b/mobile/metro.config.js @@ -0,0 +1,7 @@ +const { getDefaultConfig } = require('expo/metro-config'); + +const config = getDefaultConfig(__dirname); + +config.resolver.useWatchman = false; + +module.exports = config; \ No newline at end of file diff --git a/mobile/package.json b/mobile/package.json index 02d0bac..8d69a0a 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -1,37 +1 @@ -{ - "name": "tera-mobile", - "version": "1.0.0", - "description": "Tera - Your AI Learning Companion for Anything (Mobile App)", - "main": "expo-router/entry", - "scripts": { - "start": "expo start", - "android": "expo start --android", - "ios": "expo start --ios", - "web": "expo start --web", - "test": "jest", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@expo/metro-runtime": "^6.1.2", - "@react-native-async-storage/async-storage": "^2.2.0", - "@tanstack/react-query": "^5.62.7", - "expo": "^54.0.33", - "expo-constants": "^18.0.13", - "expo-font": "^14.0.11", - "expo-router": "^6.0.23", - "expo-secure-store": "^15.0.8", - "expo-splash-screen": "^31.0.13", - "react": "19.1.0", - "react-dom": "19.1.0", - "react-native": "0.81.5", - "react-native-gesture-handler": "^2.28.0", - "react-native-web": "^0.21.2", - "zod": "^3.24.1", - "zustand": "^5.0.2" - }, - "devDependencies": { - "@types/node": "^20.10.6", - "@types/react": "~19.1.10", - "typescript": "^5.9.3" - } -} +{ "name": "tera-mobile", "version": "1.0.0", "description": "Tera - Your AI Learning Companion for Anything (Mobile App)", "main": "expo-router/entry", "scripts": { "start": "expo start", "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web", "test": "jest", "typecheck": "tsc --noEmit" }, "dependencies": { "@expo/metro-runtime": "^6.1.2", "@react-native-async-storage/async-storage": "^2.2.0", "@tanstack/react-query": "^5.62.7", "expo": "^54.0.33", "expo-constants": "^18.0.13", "expo-font": "^14.0.11", "expo-router": "^6.0.23", "expo-secure-store": "^15.0.8", "expo-splash-screen": "^31.0.13", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "^2.28.0", "react-native-web": "^0.21.2", "zod": "^3.24.1", "zustand": "^5.0.2" }, "devDependencies": { "@types/node": "^20.10.6", "@types/react": "~19.1.10", "typescript": "^5.9.3" } } \ No newline at end of file