From 37d7284730f2ab54113eb70ce2d9077f41c2a57c Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Mon, 6 Apr 2026 08:23:55 -0700 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20add=20Smart=20Actions=20?= =?UTF-8?q?=E2=80=94=20API=20client,=20prompt=20store,=20note=20detail=20i?= =?UTF-8?q?ntegration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 of Smart Actions Roadmap: - Create mobile/src/api/note-prompts.ts: typed API client - listPromptTemplates, runPrompt, suggestTags, getReadingTime - Create mobile/src/store/prompt-store.ts: Zustand store - fetchTemplates, runPrompt, clearResult, clearError - Enhance note detail screen (mobile/src/app/note/[id].tsx): - Collapsible Smart Actions section with toggle - Reading time display on expand - Suggest tags button with inline tag display - Prompt template list (top 6) with one-tap execution - ActivityIndicator during prompt execution - Inline result card with model/token info and dismiss - Mobile typecheck passes --- mobile/src/api/note-prompts.ts | 61 +++++++++++++++++ mobile/src/app/note/[id].tsx | 108 +++++++++++++++++++++++++++++-- mobile/src/store/prompt-store.ts | 64 ++++++++++++++++++ 3 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 mobile/src/api/note-prompts.ts create mode 100644 mobile/src/store/prompt-store.ts diff --git a/mobile/src/api/note-prompts.ts b/mobile/src/api/note-prompts.ts new file mode 100644 index 0000000..849c88b --- /dev/null +++ b/mobile/src/api/note-prompts.ts @@ -0,0 +1,61 @@ +import { getApiClient } from './client'; + +export type PromptCategory = 'transform' | 'extract' | 'generate' | 'analysis' | 'vision' | 'export' | 'custom'; +export type PromptInputType = 'text' | 'image' | 'text+image' | 'multi-note'; +export type PromptOutputType = 'new_note' | 'artifact' | 'update_note'; + +export type MobilePromptTemplate = { + id: string; + slug: string; + name: string; + description: string; + category: PromptCategory; + inputType: PromptInputType; + outputType: PromptOutputType; + builtIn: boolean; +}; + +export type RunPromptInput = { + templateId: string; + noteId: string; + workspaceId: string; + imageUrl?: string; + parameters?: Record; +}; + +export type RunPromptOutput = { + content: string; + templateSlug: string; + outputType: PromptOutputType; + model?: string; + usage?: { promptTokens: number; completionTokens: number; totalTokens: number }; +}; + +export async function listPromptTemplates(): Promise { + const res = await getApiClient().fetch<{ items: MobilePromptTemplate[] }>('/note-prompts'); + return res.items; +} + +export async function runPrompt(input: RunPromptInput): Promise { + return getApiClient().fetch('/note-prompts/run', { + method: 'POST', + body: JSON.stringify(input), + }); +} + +export async function suggestTags(noteId: string, workspaceId: string): Promise { + const res = await getApiClient().fetch<{ tags: string[] }>( + `/notes/${encodeURIComponent(noteId)}/suggest-tags`, + { method: 'POST', body: JSON.stringify({ workspaceId }) }, + ); + return res.tags; +} + +export async function getReadingTime( + noteId: string, + workspaceId: string, +): Promise<{ wordCount: number; readingTimeMinutes: number }> { + return getApiClient().fetch( + `/notes/${encodeURIComponent(noteId)}/reading-time?workspaceId=${encodeURIComponent(workspaceId)}`, + ); +} diff --git a/mobile/src/app/note/[id].tsx b/mobile/src/app/note/[id].tsx index a1a783f..83a7a1e 100644 --- a/mobile/src/app/note/[id].tsx +++ b/mobile/src/app/note/[id].tsx @@ -1,7 +1,9 @@ import { useEffect, useState } from 'react'; import { useLocalSearchParams } from 'expo-router'; -import { Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native'; +import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native'; import { useNotesStore, type NotesState } from '../../store/notes-store'; +import { usePromptStore, type PromptState } from '../../store/prompt-store'; +import { getReadingTime, suggestTags } from '../../api/note-prompts'; import { colors } from '../../theme'; export default function NoteDetailScreen() { @@ -11,9 +13,18 @@ export default function NoteDetailScreen() { const isLoading = useNotesStore((state: NotesState) => state.isLoading); const openNote = useNotesStore((state: NotesState) => state.openNote); const updateNote = useNotesStore((state: NotesState) => state.updateNote); + const templates = usePromptStore((state: PromptState) => state.templates); + const isRunning = usePromptStore((state: PromptState) => state.isRunning); + const lastResult = usePromptStore((state: PromptState) => state.lastResult); + const fetchTemplates = usePromptStore((state: PromptState) => state.fetchTemplates); + const runPromptAction = usePromptStore((state: PromptState) => state.runPrompt); + const clearResult = usePromptStore((state: PromptState) => state.clearResult); const [isEditing, setIsEditing] = useState(false); const [draftTitle, setDraftTitle] = useState(''); const [draftBody, setDraftBody] = useState(''); + const [readingTime, setReadingTime] = useState<{ wordCount: number; readingTimeMinutes: number } | null>(null); + const [suggestedTags, setSuggestedTags] = useState([]); + const [showSmartActions, setShowSmartActions] = useState(false); useEffect(() => { void openNote(noteId); @@ -98,10 +109,99 @@ export default function NoteDetailScreen() { Last updated: {formattedUpdatedAt} + {/* Smart Actions */} - Current mobile scope - Artifacts and review context continue to load from the fuller web surface. - This mobile screen now focuses on reading and updating the persisted note body. + { + setShowSmartActions(!showSmartActions); + if (!showSmartActions && templates.length === 0) { + void fetchTemplates(); + } + if (!showSmartActions && selectedNote) { + void getReadingTime(noteId, selectedNote.workspaceId).then(setReadingTime).catch(() => {}); + } + }} + > + ✨ Smart Actions + {showSmartActions ? '▲' : '▼'} + + + {showSmartActions && ( + + {readingTime && ( + + ⏱ {readingTime.readingTimeMinutes} min read · {readingTime.wordCount} words + + )} + + { + if (!selectedNote) return; + void suggestTags(noteId, selectedNote.workspaceId) + .then(setSuggestedTags) + .catch(() => {}); + }} + > + Suggest tags + + + {suggestedTags.length > 0 && ( + + {suggestedTags.map((tag) => ( + + {tag} + + ))} + + )} + + {templates.slice(0, 6).map((t) => ( + { + if (!selectedNote) return; + void runPromptAction({ + templateId: t.slug, + noteId, + workspaceId: selectedNote.workspaceId, + }); + }} + > + {t.name} + {t.description} + + ))} + + {isRunning && ( + + + Running… + + )} + + {lastResult && ( + + Result + {lastResult.content} + {lastResult.model && ( + + {lastResult.model} · {lastResult.usage?.totalTokens ?? 0} tokens + + )} + + Dismiss + + + )} + + )} ); diff --git a/mobile/src/store/prompt-store.ts b/mobile/src/store/prompt-store.ts new file mode 100644 index 0000000..adf7716 --- /dev/null +++ b/mobile/src/store/prompt-store.ts @@ -0,0 +1,64 @@ +import { create } from 'zustand'; +import { + listPromptTemplates, + runPrompt, + type MobilePromptTemplate, + type RunPromptInput, + type RunPromptOutput, +} from '../api/note-prompts'; + +export type PromptState = { + templates: MobilePromptTemplate[]; + isLoading: boolean; + isRunning: boolean; + lastResult: RunPromptOutput | null; + error: string | null; + fetchTemplates: () => Promise; + runPrompt: (input: RunPromptInput) => Promise; + clearResult: () => void; + clearError: () => void; +}; + +export const usePromptStore = create((set) => ({ + templates: [], + isLoading: false, + isRunning: false, + lastResult: null, + error: null, + + async fetchTemplates() { + set({ isLoading: true, error: null }); + try { + const templates = await listPromptTemplates(); + set({ templates, isLoading: false }); + } catch (err) { + set({ + isLoading: false, + error: err instanceof Error ? err.message : 'Failed to load templates', + }); + } + }, + + async runPrompt(input: RunPromptInput) { + set({ isRunning: true, error: null, lastResult: null }); + try { + const result = await runPrompt(input); + set({ lastResult: result, isRunning: false }); + return result; + } catch (err) { + set({ + isRunning: false, + error: err instanceof Error ? err.message : 'Prompt execution failed', + }); + return null; + } + }, + + clearResult() { + set({ lastResult: null }); + }, + + clearError() { + set({ error: null }); + }, +}));