-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/#35 ai feedback #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
262ce64
b00aadc
07dae26
ad0932d
9fd0a4b
88b7fcc
818c133
973d0f7
8300b20
465ae09
477b16d
2935bbc
850b73e
131bdad
48adf1a
8ac7a94
7b59b8b
476567b
e4f904b
02d17ec
ed3417e
aa388cf
ace86a6
5678462
421cd5b
f7c447a
bef1ef3
85b03c9
f412b1c
5266785
6bae3c0
bde7afa
6e24145
4dd78fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| EXPO_PUBLIC_SIGNING_SECRET= | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,22 +44,30 @@ src/app/ | |
| entry/ | ||
| create.tsx # Create entry /entry/create?journalId=...&journalName=... | ||
| [id].tsx # Entry detail /entry/[id]?journalName=... | ||
| explore.tsx # Report tab | ||
| days/ | ||
| _layout.tsx # Stack — scoped to days tab | ||
| index.tsx # Daily reflection list | ||
| settings.tsx # AI reflection settings (model, time, enabled) | ||
| search/ | ||
| _layout.tsx # Stack — scoped to search tab | ||
| index.tsx # Journal entry search | ||
| ``` | ||
|
|
||
| **Navigation flow:** Journal list → `[id]` (entry list) → `entry/[id]` (entry detail) | ||
|
|
||
| **Key rule:** `NativeTabs` is the root navigator; `Stack` lives inside `(journal)` group. This keeps the tab bar visible when pushing screens. | ||
| **Key rule:** `NativeTabs` is the root navigator; `Stack` lives inside each tab group (`(journal)`, `days`, `search`). This keeps the tab bar visible when pushing screens. | ||
|
|
||
| ### Database Layer | ||
|
|
||
| Drizzle ORM + expo-sqlite. Schema is split by domain in `src/db/schemas/`: | ||
|
|
||
| | File | Tables | | ||
| | ------------- | ---------------------------------------- | | ||
| | `journals.ts` | `journals` | | ||
| | `fields.ts` | `fields` (field definitions per journal) | | ||
| | `entries.ts` | `entries`, `entry_values` | | ||
| | File | Tables | | ||
| | ---------------- | ------------------------------------------------- | | ||
| | `journals.ts` | `journals` | | ||
| | `fields.ts` | `fields` (field definitions per journal) | | ||
| | `entries.ts` | `entries`, `entry_values` | | ||
| | `reflections.ts` | `reflections` (one AI-generated reflection/day) | | ||
| | `settings.ts` | `settings` (KVS — key/value pairs for app config) | | ||
|
|
||
| ```mermaid | ||
| erDiagram | ||
|
|
@@ -95,6 +103,22 @@ erDiagram | |
| TEXT value | ||
| } | ||
|
|
||
| reflections { | ||
| TEXT id PK | ||
| INTEGER date "unique, midnight timestamp" | ||
| TEXT title | ||
| TEXT firstCategory | ||
| TEXT firstContent | ||
| TEXT secondCategory | ||
| TEXT secondContent | ||
| INTEGER createdAt | ||
| } | ||
|
|
||
| settings { | ||
| TEXT key PK | ||
| TEXT value | ||
| } | ||
|
|
||
| journals ||--o{ fields : "has" | ||
| journals ||--o{ entries : "has" | ||
| entries ||--o{ entry_values : "has" | ||
|
|
@@ -160,6 +184,24 @@ All field values are stored as `text | null` in `entry_values.value`. Serializat | |
| - Remaining field values joined with spaces = preview (also used for search) | ||
| - Entries grouped by month for the list view (`groupByMonth`) | ||
|
|
||
| ### AI Reflection Pipeline | ||
|
|
||
| On-device LLM generates daily reflections from journal entries: | ||
|
|
||
| 1. **Entry collection** — `src/db/queries/entries.ts` fetches all entries for a given day (`DailyEntryObj[]`) | ||
| 2. **Text conversion** — `src/utils/days/reflection/get-reflection.ts` formats entries into structured text (journal name + field labels/values) | ||
| 3. **LLM inference** — `@react-native-ai/llama` downloads a GGUF model, Vercel `ai` package's `generateText()` runs inference with a system prompt | ||
| 4. **Parsing** — JSON output is extracted via regex and validated with `reflectionSchema` (Zod) | ||
| 5. **Storage** — Result saved to `reflections` table (one per day, keyed by midnight timestamp) | ||
|
|
||
| **Supported models** (defined in `src/constants/ai-models.ts`): Gemma 3 4B, Qwen 3 4B, Phi-4 Mini — all Q4_K_M quantization from HuggingFace. | ||
|
|
||
| **Settings** are stored in the `settings` KVS table, managed by `src/hooks/settings/use-ai-reflection-settings.ts`: | ||
|
|
||
| - `aiReflectionEnabled` — on/off toggle | ||
| - `aiReflectionModelId` — selected GGUF model | ||
| - `aiReflectionTime` — time of day to auto-generate (via `use-auto-reflection.ts`) | ||
|
|
||
|
Comment on lines
+187
to
+204
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 5 'Apple|apple|`@react-native-ai/llama`|llama\.languageModel|downloadModel' \
src package.json pnpm-workspace.yaml .env.example || trueRepository: 273Do/Nicky Length of output: 3978 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- Apple/Llama references in tracked source and docs ---'
rg -n -i -C 4 'apple|llama|provider|languageModel|downloadModel' \
--glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' . || true
printf '%s\n' '--- Relevant source and documentation sections ---'
for f in src/utils/days/reflection/get-reflection.ts CLAUDE.md; do
if [ -f "$f" ]; then
echo "### $f"
if [ "$f" = "CLAUDE.md" ]; then
sed -n '180,230p' "$f"
else
cat -n "$f"
fi
fi
done
printf '%s\n' '--- Apple package metadata and lockfile entries ---'
rg -n -C 3 '`@react-native-ai/apple`|`@react-native-ai/llama`' \
package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || true
printf '%s\n' '--- Read-only structural check of provider imports and calls ---'
python3 - <<'PY'
from pathlib import Path
files = [p for p in Path("src").rglob("*") if p.is_file()]
apple_imports = []
llama_imports = []
llama_calls = []
for path in files:
try:
text = path.read_text()
except UnicodeDecodeError:
continue
if "`@react-native-ai/apple`" in text:
apple_imports.append(str(path))
if "`@react-native-ai/llama`" in text:
llama_imports.append(str(path))
if "llama.languageModel" in text or "downloadModel(" in text:
llama_calls.append(str(path))
print("apple_import_files =", apple_imports)
print("llama_import_files =", llama_imports)
print("llama_call_files =", llama_calls)
PYRepository: 273Do/Nicky Length of output: 36552 Apple Intelligence を優先するプロバイダー選択を実装する
🤖 Prompt for AI Agents |
||
| ### Keyboard Dismiss Rule | ||
|
|
||
| Always call `Keyboard.dismiss()` **before** any `async` save operation that triggers navigation. Skipping this causes a `RemoteTextInput` session crash on iOS when the keyboard is mid-input as the screen unmounts. | ||
|
|
@@ -176,6 +218,8 @@ Always call `Keyboard.dismiss()` **before** any `async` save operation that trig | |
| | `expo-sqlite` + `drizzle-orm` | Local SQLite persistence | | ||
| | `expo-crypto` | `Crypto.randomUUID()` for ID generation at the app layer | | ||
| | `PlatformColor` | Adaptive system colors: `"label"`, `"systemBackground"`, `"systemIndigo"` | | ||
| | `@react-native-ai/llama` | On-device GGUF model download + inference via `downloadModel()` and `llama.languageModel()` | | ||
| | `ai` (Vercel AI SDK) | `generateText()` with structured prompts — used with llama model provider | | ||
|
|
||
| ### SwiftUI Component Rules | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| CREATE TABLE `settings` ( | ||
| `key` text PRIMARY KEY NOT NULL, | ||
| `value` text | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| CREATE TABLE `reflections` ( | ||
| `id` text PRIMARY KEY NOT NULL, | ||
| `date` integer NOT NULL, | ||
| `title` text NOT NULL, | ||
| `firstCategory` text NOT NULL, | ||
| `firstContent` text NOT NULL, | ||
| `secondCategory` text NOT NULL, | ||
| `secondContent` text NOT NULL, | ||
| `createdAt` integer NOT NULL | ||
| ); | ||
| --> statement-breakpoint | ||
| CREATE UNIQUE INDEX `reflections_date_unique` ON `reflections` (`date`); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,268 @@ | ||
| { | ||
| "version": "6", | ||
| "dialect": "sqlite", | ||
| "id": "c7f00b54-9b8b-464c-a516-f825371e613b", | ||
| "prevId": "3d118337-e09e-4635-9ca5-d9d5a5c94bf2", | ||
| "tables": { | ||
| "entries": { | ||
| "name": "entries", | ||
| "columns": { | ||
| "id": { | ||
| "name": "id", | ||
| "type": "text", | ||
| "primaryKey": true, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "journalId": { | ||
| "name": "journalId", | ||
| "type": "text", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "bookmark": { | ||
| "name": "bookmark", | ||
| "type": "integer", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false, | ||
| "default": false | ||
| }, | ||
| "createdAt": { | ||
| "name": "createdAt", | ||
| "type": "integer", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "updatedAt": { | ||
| "name": "updatedAt", | ||
| "type": "integer", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| } | ||
| }, | ||
| "indexes": {}, | ||
| "foreignKeys": { | ||
| "entries_journalId_journals_id_fk": { | ||
| "name": "entries_journalId_journals_id_fk", | ||
| "tableFrom": "entries", | ||
| "tableTo": "journals", | ||
| "columnsFrom": ["journalId"], | ||
| "columnsTo": ["id"], | ||
| "onDelete": "cascade", | ||
| "onUpdate": "no action" | ||
| } | ||
| }, | ||
| "compositePrimaryKeys": {}, | ||
| "uniqueConstraints": {}, | ||
| "checkConstraints": {} | ||
| }, | ||
| "entry_values": { | ||
| "name": "entry_values", | ||
| "columns": { | ||
| "id": { | ||
| "name": "id", | ||
| "type": "text", | ||
| "primaryKey": true, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "entryId": { | ||
| "name": "entryId", | ||
| "type": "text", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "fieldId": { | ||
| "name": "fieldId", | ||
| "type": "text", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "value": { | ||
| "name": "value", | ||
| "type": "text", | ||
| "primaryKey": false, | ||
| "notNull": false, | ||
| "autoincrement": false | ||
| } | ||
| }, | ||
| "indexes": { | ||
| "entry_field_unique": { | ||
| "name": "entry_field_unique", | ||
| "columns": ["entryId", "fieldId"], | ||
| "isUnique": true | ||
| } | ||
| }, | ||
| "foreignKeys": { | ||
| "entry_values_entryId_entries_id_fk": { | ||
| "name": "entry_values_entryId_entries_id_fk", | ||
| "tableFrom": "entry_values", | ||
| "tableTo": "entries", | ||
| "columnsFrom": ["entryId"], | ||
| "columnsTo": ["id"], | ||
| "onDelete": "cascade", | ||
| "onUpdate": "no action" | ||
| }, | ||
| "entry_values_fieldId_fields_id_fk": { | ||
| "name": "entry_values_fieldId_fields_id_fk", | ||
| "tableFrom": "entry_values", | ||
| "tableTo": "fields", | ||
| "columnsFrom": ["fieldId"], | ||
| "columnsTo": ["id"], | ||
| "onDelete": "cascade", | ||
| "onUpdate": "no action" | ||
| } | ||
| }, | ||
| "compositePrimaryKeys": {}, | ||
| "uniqueConstraints": {}, | ||
| "checkConstraints": {} | ||
| }, | ||
| "fields": { | ||
| "name": "fields", | ||
| "columns": { | ||
| "id": { | ||
| "name": "id", | ||
| "type": "text", | ||
| "primaryKey": true, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "journalId": { | ||
| "name": "journalId", | ||
| "type": "text", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "type": { | ||
| "name": "type", | ||
| "type": "text", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "label": { | ||
| "name": "label", | ||
| "type": "text", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "sortOrder": { | ||
| "name": "sortOrder", | ||
| "type": "integer", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| } | ||
| }, | ||
| "indexes": {}, | ||
| "foreignKeys": { | ||
| "fields_journalId_journals_id_fk": { | ||
| "name": "fields_journalId_journals_id_fk", | ||
| "tableFrom": "fields", | ||
| "tableTo": "journals", | ||
| "columnsFrom": ["journalId"], | ||
| "columnsTo": ["id"], | ||
| "onDelete": "cascade", | ||
| "onUpdate": "no action" | ||
| } | ||
| }, | ||
| "compositePrimaryKeys": {}, | ||
| "uniqueConstraints": {}, | ||
| "checkConstraints": {} | ||
| }, | ||
| "journals": { | ||
| "name": "journals", | ||
| "columns": { | ||
| "id": { | ||
| "name": "id", | ||
| "type": "text", | ||
| "primaryKey": true, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "name": { | ||
| "name": "name", | ||
| "type": "text", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "icon": { | ||
| "name": "icon", | ||
| "type": "text", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "color": { | ||
| "name": "color", | ||
| "type": "text", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "createdAt": { | ||
| "name": "createdAt", | ||
| "type": "integer", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "updatedAt": { | ||
| "name": "updatedAt", | ||
| "type": "integer", | ||
| "primaryKey": false, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| } | ||
| }, | ||
| "indexes": {}, | ||
| "foreignKeys": {}, | ||
| "compositePrimaryKeys": {}, | ||
| "uniqueConstraints": {}, | ||
| "checkConstraints": {} | ||
| }, | ||
| "settings": { | ||
| "name": "settings", | ||
| "columns": { | ||
| "key": { | ||
| "name": "key", | ||
| "type": "text", | ||
| "primaryKey": true, | ||
| "notNull": true, | ||
| "autoincrement": false | ||
| }, | ||
| "value": { | ||
| "name": "value", | ||
| "type": "text", | ||
| "primaryKey": false, | ||
| "notNull": false, | ||
| "autoincrement": false | ||
| } | ||
| }, | ||
| "indexes": {}, | ||
| "foreignKeys": {}, | ||
| "compositePrimaryKeys": {}, | ||
| "uniqueConstraints": {}, | ||
| "checkConstraints": {} | ||
| } | ||
| }, | ||
| "views": {}, | ||
| "enums": {}, | ||
| "_meta": { | ||
| "schemas": {}, | ||
| "tables": {}, | ||
| "columns": {} | ||
| }, | ||
| "internal": { | ||
| "indexes": {} | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: 273Do/Nicky
Length of output: 2780
🏁 Script executed:
Repository: 273Do/Nicky
Length of output: 2368
署名秘密鍵をクライアントに配布しないでください。
EXPO_PUBLIC_SIGNING_SECRETはクライアントバンドルに含まれます。export-journal.tsとimport-journal.tsはこの値を署名秘密鍵として共有ファイルの検証に使用するため、利用者は値を取得して有効な署名を生成できます。署名を信頼境界として使用する場合は、署名処理と秘密鍵をサーバー側へ移動してください。
🤖 Prompt for AI Agents