From 262ce64f94318373417283474496a72dd07431b9 Mon Sep 17 00:00:00 2001 From: 273* Date: Tue, 11 Aug 2026 13:23:45 +0900 Subject: [PATCH 01/32] =?UTF-8?q?feat:=20llm=E5=91=A8=E3=82=8A=E3=81=AE?= =?UTF-8?q?=E3=83=91=E3=83=83=E3=82=B1=E3=83=BC=E3=82=B8=E3=82=92=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=82=B9=E3=83=88=E3=83=BC=E3=83=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 6 ++ pnpm-lock.yaml | 207 ++++++++++++++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 3 +- 3 files changed, 215 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index be10283..2cca096 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,14 @@ }, "dependencies": { "@expo/ui": "~55.0.17", + "@react-native-ai/apple": "^0.12.0", + "@react-native-ai/llama": "^0.12.0", "@react-navigation/bottom-tabs": "^7.15.5", "@react-navigation/elements": "^2.9.10", "@react-navigation/native": "^7.1.33", + "@stardazed/streams-text-encoding": "^1.0.2", + "@ungap/structured-clone": "^1.3.3", + "ai": "^7.0.58", "babel-plugin-inline-import": "^3.0.0", "drizzle-kit": "^0.31.10", "drizzle-orm": "^0.45.2", @@ -43,6 +48,7 @@ "expo-symbols": "~55.0.7", "expo-system-ui": "~55.0.16", "expo-web-browser": "~55.0.14", + "llama.rn": "0.12.0-rc.9", "react": "19.2.0", "react-dom": "19.2.0", "react-native": "0.83.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f1e312..fbe7aaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,12 @@ importers: '@expo/ui': specifier: ~55.0.17 version: 55.0.17(expo@55.0.18)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@react-native-ai/apple': + specifier: ^0.12.0 + version: 0.12.0(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)) + '@react-native-ai/llama': + specifier: ^0.12.0 + version: 0.12.0(llama.rn@0.12.0-rc.9(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) '@react-navigation/bottom-tabs': specifier: ^7.15.5 version: 7.15.11(@react-navigation/native@7.2.2(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-screens@4.23.0(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) @@ -30,6 +36,15 @@ importers: '@react-navigation/native': specifier: ^7.1.33 version: 7.2.2(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@stardazed/streams-text-encoding': + specifier: ^1.0.2 + version: 1.0.2 + '@ungap/structured-clone': + specifier: ^1.3.3 + version: 1.3.3 + ai: + specifier: ^7.0.58 + version: 7.0.58(zod@4.4.3) babel-plugin-inline-import: specifier: ^3.0.0 version: 3.0.0 @@ -102,6 +117,9 @@ importers: expo-web-browser: specifier: ~55.0.14 version: 55.0.14(expo@55.0.18)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)) + llama.rn: + specifier: 0.12.0-rc.9 + version: 0.12.0-rc.9(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) react: specifier: 19.2.0 version: 19.2.0 @@ -145,6 +163,32 @@ importers: packages: + '@ai-sdk/gateway@4.0.46': + resolution: {integrity: sha512-LIAO6kAG8fpXQb9L0iwPk1FIbXftvqnyC56v5NEAzeWTeL8fUsy/Hx86VPBTWEDFdwbVprjWifJOAqS6AOj3mA==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.42': + resolution: {integrity: sha512-Q07lZsq4ir+xwAGVfSbY2WNGLrbfWINFaL2I/vTBFrkuXeP7VZh+UtQgLOKZS6Z/6flNFW22jDYdbINvwTV/PA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@5.0.25': + resolution: {integrity: sha512-xscPPHCSjCHWrdhai25sbHCJeKNLW/3D1uSpUZa4cEtTKXA8OnPQ3+Rfu1SmM5Ea/Mf8Dfn3cllw9zeMzo/zFA==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.14': + resolution: {integrity: sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA==} + engines: {node: '>=18'} + + '@ai-sdk/provider@4.0.7': + resolution: {integrity: sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q==} + engines: {node: '>=22'} + '@babel/code-frame@7.10.4': resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==} @@ -1374,6 +1418,10 @@ packages: resolution: {integrity: sha512-wC562eD3gS6vO2tWHToFhlFnmHKfKHgF1oyvojeSkLK/ZYop1bMU+7cOMiF9Sq70CzcsLy/EMRy/uRc76QmNRw==} hasBin: true + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1982,6 +2030,17 @@ packages: '@types/react': optional: true + '@react-native-ai/apple@0.12.0': + resolution: {integrity: sha512-BC/kEDbCZprv1xcQCWsgapXm/WEsj5lieBDvU1vJXStrN+BG+hWM01zoqDGTD/j71zvt9DEfHcMvEqbrGY2uAA==} + peerDependencies: + react-native: '>=0.76.0' + + '@react-native-ai/llama@0.12.0': + resolution: {integrity: sha512-v/zhk9SEAk7P4/iELHMkAfIEEIn1TaMdIwfaFv41Yu3wm3sRbws7qRufTeq3nFBnSLj3MtVMZy4Vew7Fy5uyaA==} + peerDependencies: + llama.rn: ^0.10.1 + react-native: '>=0.76.0' + '@react-native/assets-registry@0.83.6': resolution: {integrity: sha512-iljb4ue1yWJ3EhySz7EjV6CzSVrI2uNtR8BI2jzP5+QS5E4Cl3fdIJRmVwDEx1pu8uE97PGEusGRHnoaZ9Q3jg==} engines: {node: '>= 20.19.4'} @@ -2203,6 +2262,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@stardazed/streams-text-encoding@1.0.2': + resolution: {integrity: sha512-f2Z15BId3t44a/u21yYSGXFAkCyKocmAyduoAy7swnZ4xIfbaZlOWsgly/jDNNOuj6hYQN72UaBRe3Z/tOHfqg==} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -2337,6 +2399,10 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher + + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -2441,6 +2507,10 @@ packages: cpu: [x64] os: [win32] + '@vercel/oidc@3.2.0': + resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} + engines: {node: '>= 20'} + '@vitest/browser-preview@4.1.10': resolution: {integrity: sha512-14MJrL59ZFkqXLjwfSk6RzTDy5Czf9UG4+8q8L6Gxjs2aPjEce/cVNYV14bXAc2BvMjUNu904+ZEZA1Xc1wtvQ==} peerDependencies: @@ -2683,6 +2753,9 @@ packages: cpu: [x64] os: [win32] + '@workflow/serde@4.1.0': + resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==} + '@xmldom/xmldom@0.8.13': resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} @@ -2842,6 +2915,12 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ai@7.0.58: + resolution: {integrity: sha512-GfgO90CQQ0yYuoxJAUOeQ6tviyYw1BUIDygSZ1q3Ce6kSc93tYmB5eltKY/NxC0YOouAax7JvDqVnYxvIAr04Q==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} @@ -3022,6 +3101,9 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -3635,6 +3717,10 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + expo-asset@55.0.16: resolution: {integrity: sha512-5IJyfJtYqvKGg04NKGQWiCIoK/fULDL9m15mXPPyfabD1jsToVj2hnWmo1r2SWNNmMwtQxi6jTpcGwVo2nLDxg==} peerDependencies: @@ -4317,6 +4403,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -4503,6 +4592,14 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} + llama.rn@0.12.0-rc.9: + resolution: {integrity: sha512-6qmo0sOGs7mk7stAGPtQ13R002yV2sYWR5z0YmUNlBUWVeOVeSXfsG2CTAfbyEr6fjXhFNgZVPYEt0Hd7Yd27A==} + engines: {node: '>= 16.0.0'} + hasBin: true + peerDependencies: + react: '*' + react-native: '*' + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -5040,6 +5137,12 @@ packages: react-is@19.2.5: resolution: {integrity: sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==} + react-native-blob-util@0.24.10: + resolution: {integrity: sha512-4yazgoCstXgt2/CRURtowGP3furAzbWdfMQ4jyUPDo5xtLx7KE2F/xMz3oU3shLwfL4id0iEKsBywoxvJSHPzQ==} + peerDependencies: + react: '*' + react-native: '*' + react-native-gesture-handler@2.30.1: resolution: {integrity: sha512-xIUBDo5ktmJs++0fZlavQNvDEE4PsihWhSeJsJtoz4Q6p0MiTM9TgrTgfEgzRR36qGPytFoeq+ShLrVwGdpUdA==} peerDependencies: @@ -5599,6 +5702,14 @@ packages: undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -5876,8 +5987,43 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + snapshots: + '@ai-sdk/gateway@4.0.46(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.7 + '@ai-sdk/provider-utils': 5.0.25(zod@4.4.3) + '@vercel/oidc': 3.2.0 + zod: 4.4.3 + + '@ai-sdk/provider-utils@4.0.42(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 3.0.14 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.1.0 + undici: 5.29.0 + zod: 4.4.3 + + '@ai-sdk/provider-utils@5.0.25(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.7 + '@standard-schema/spec': 1.1.0 + '@workflow/serde': 4.1.0 + eventsource-parser: 3.1.0 + undici: 7.29.0 + zod: 4.4.3 + + '@ai-sdk/provider@3.0.14': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/provider@4.0.7': + dependencies: + json-schema: 0.4.0 + '@babel/code-frame@7.10.4': dependencies: '@babel/highlight': 7.25.9 @@ -7219,6 +7365,8 @@ snapshots: chalk: 4.1.2 js-yaml: 4.1.1 + '@fastify/busboy@2.1.1': {} + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -7671,6 +7819,24 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@react-native-ai/apple@0.12.0(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))': + dependencies: + '@ai-sdk/provider': 3.0.14 + '@ai-sdk/provider-utils': 4.0.42(zod@4.4.3) + react-native: 0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) + zod: 4.4.3 + + '@react-native-ai/llama@0.12.0(llama.rn@0.12.0-rc.9(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': + dependencies: + '@ai-sdk/provider': 3.0.14 + '@ai-sdk/provider-utils': 4.0.42(zod@4.4.3) + llama.rn: 0.12.0-rc.9(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + react-native: 0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) + react-native-blob-util: 0.24.10(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + zod: 4.4.3 + transitivePeerDependencies: + - react + '@react-native/assets-registry@0.83.6': {} '@react-native/babel-plugin-codegen@0.83.6(@babel/core@7.29.0)': @@ -7919,6 +8085,8 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@stardazed/streams-text-encoding@1.0.2': {} + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.0 @@ -8100,6 +8268,8 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@ungap/structured-clone@1.3.3': {} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -8159,6 +8329,8 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vercel/oidc@3.2.0': {} + '@vitest/browser-preview@4.1.10(@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.6.0)(esbuild@0.27.7)(terser@5.46.2)(tsx@4.21.0)(typescript@5.9.3)(vite@8.2.0(@types/node@25.6.0)(esbuild@0.27.7)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))(vite@8.2.0(@types/node@25.6.0)(esbuild@0.27.7)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@testing-library/dom': 10.4.1 @@ -8325,6 +8497,8 @@ snapshots: '@voidzero-dev/vite-plus-win32-x64-msvc@0.2.7': optional: true + '@workflow/serde@4.1.0': {} + '@xmldom/xmldom@0.8.13': {} '@xmldom/xmldom@0.9.10': {} @@ -8419,6 +8593,13 @@ snapshots: agent-base@7.1.4: {} + ai@7.0.58(zod@4.4.3): + dependencies: + '@ai-sdk/gateway': 4.0.46(zod@4.4.3) + '@ai-sdk/provider': 4.0.7 + '@ai-sdk/provider-utils': 5.0.25(zod@4.4.3) + zod: 4.4.3 + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -8686,6 +8867,8 @@ snapshots: balanced-match@4.0.4: {} + base-64@1.0.0: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.10.24: {} @@ -9392,6 +9575,8 @@ snapshots: event-target-shim@5.0.1: {} + eventsource-parser@3.1.0: {} + expo-asset@55.0.16(expo@55.0.18)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3): dependencies: '@expo/image-utils': 0.8.13(typescript@5.9.3) @@ -10165,6 +10350,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -10300,6 +10487,11 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 + llama.rn@0.12.0-rc.9(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + dependencies: + react: 19.2.0 + react-native: 0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -10932,6 +11124,13 @@ snapshots: react-is@19.2.5: {} + react-native-blob-util@0.24.10(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + dependencies: + base-64: 1.0.0 + glob: 13.0.6 + react: 19.2.0 + react-native: 0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) + react-native-gesture-handler@2.30.1(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: '@egjs/hammerjs': 2.0.17 @@ -11596,6 +11795,12 @@ snapshots: undici-types@7.19.2: {} + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 + + undici@7.29.0: {} + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: @@ -11916,3 +12121,5 @@ snapshots: '@yuku-parser/binding-win32-x64': 0.5.48 zod@3.25.76: {} + + zod@4.4.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 386d7b1..b24ec0a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,10 +1,11 @@ allowBuilds: esbuild: true + llama.rn: true unrs-resolver: true catalog: vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest vite-plus: latest + vitest: npm:@voidzero-dev/vite-plus-test@latest overrides: vite: "catalog:" vitest: "catalog:" From b00aadc7cc69af6afb91e544270c587db54261d6 Mon Sep 17 00:00:00 2001 From: 273* Date: Tue, 11 Aug 2026 13:25:43 +0900 Subject: [PATCH 02/32] =?UTF-8?q?feat:=20reflection=20=E3=81=A7=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E3=81=99=E3=82=8B=E3=83=97=E3=83=AD=E3=83=90=E3=82=A4?= =?UTF-8?q?=E3=83=80=E3=83=BC=E3=81=A8=E3=83=A2=E3=83=87=E3=83=AB=E3=82=92?= =?UTF-8?q?=E5=8F=96=E5=BE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 5 ++++ .../days/reflection/get-reflection-model.ts | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 .env.example create mode 100644 src/utils/days/reflection/get-reflection-model.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ae9106f --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +EXPO_PUBLIC_SIGNING_SECRET= + +# Download model from HuggingFace +# https://huggingface.co/ggml-org/Qwen3-1.7B-GGUF +EXPO_PUBLIC_REFLECTION_LLM=ggml-org/Qwen3-1.7B-GGUF/Qwen3-1.7B-Q4_K_M.gguf diff --git a/src/utils/days/reflection/get-reflection-model.ts b/src/utils/days/reflection/get-reflection-model.ts new file mode 100644 index 0000000..bac0d98 --- /dev/null +++ b/src/utils/days/reflection/get-reflection-model.ts @@ -0,0 +1,25 @@ +import { apple } from "@react-native-ai/apple"; +import { llama } from "@react-native-ai/llama"; + +const MODEL = process.env.EXPO_PUBLIC_REFLECTION_LLM; + +/** + * days のフィードバックに使用するプロバイダーを取得する関数 + * デフォルトでは Apple Intelligence を使用し、非対応の場合 llama を使用する。 + */ +export const getReflectionModel = () => { + // Apple Intelligence が使用可能かどうか + const appleAvailable = apple.isAvailable(); + + if (appleAvailable) { + return apple(); + } + + // 非対応の場合は Llama を使用 + if (MODEL) { + const model = llama.languageModel(MODEL); + return model; + } + + return null; +}; From 07dae26ce01d576b7b48b3014952ce98df0a3e22 Mon Sep 17 00:00:00 2001 From: 273* Date: Tue, 11 Aug 2026 13:43:12 +0900 Subject: [PATCH 03/32] =?UTF-8?q?fix:=20llama=20=E3=81=AB=E3=81=A6?= =?UTF-8?q?=E3=83=A2=E3=83=87=E3=83=AB=E3=81=AE=E8=AA=AD=E3=81=BF=E8=BE=BC?= =?UTF-8?q?=E3=81=BF=E6=96=B9=E6=B3=95=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/days/reflection/get-reflection-model.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/utils/days/reflection/get-reflection-model.ts b/src/utils/days/reflection/get-reflection-model.ts index bac0d98..82bf35c 100644 --- a/src/utils/days/reflection/get-reflection-model.ts +++ b/src/utils/days/reflection/get-reflection-model.ts @@ -1,5 +1,5 @@ import { apple } from "@react-native-ai/apple"; -import { llama } from "@react-native-ai/llama"; +import { downloadModel, llama } from "@react-native-ai/llama"; const MODEL = process.env.EXPO_PUBLIC_REFLECTION_LLM; @@ -7,7 +7,7 @@ const MODEL = process.env.EXPO_PUBLIC_REFLECTION_LLM; * days のフィードバックに使用するプロバイダーを取得する関数 * デフォルトでは Apple Intelligence を使用し、非対応の場合 llama を使用する。 */ -export const getReflectionModel = () => { +export const getReflectionModel = async () => { // Apple Intelligence が使用可能かどうか const appleAvailable = apple.isAvailable(); @@ -17,7 +17,12 @@ export const getReflectionModel = () => { // 非対応の場合は Llama を使用 if (MODEL) { - const model = llama.languageModel(MODEL); + const modelPath = await downloadModel(MODEL); + + const model = llama.languageModel(modelPath); + + await model.prepare(); + return model; } From 9fd0a4b2cbde08df81cc27ae3cee0927b8f1c89b Mon Sep 17 00:00:00 2001 From: 273* Date: Tue, 11 Aug 2026 17:10:13 +0900 Subject: [PATCH 04/32] =?UTF-8?q?refactor:=20=E5=AE=9A=E6=95=B0=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E7=A7=BB=E5=8B=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/field/emtry-number.tsx | 2 +- src/components/field/entry-date.tsx | 2 +- src/components/field/entry-long-text.tsx | 2 +- src/components/field/entry-text.tsx | 2 +- src/components/field/entry-time.tsx | 2 +- src/components/journal/field-bottom-sheet.tsx | 2 +- src/components/journal/icon-select-bottom-sheet.tsx | 2 +- src/components/journal/journal-create-view.tsx | 2 +- src/core/{constants.ts => constants/journal.ts} | 0 src/db/schemas/fields.ts | 4 ++-- src/db/seed.ts | 2 +- src/utils/days/import-journal.ts | 2 +- src/utils/entry/preview.ts | 2 +- src/utils/entry/use-entry.ts | 2 +- src/utils/journal/use-journal-field.ts | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) rename src/core/{constants.ts => constants/journal.ts} (100%) diff --git a/src/components/field/emtry-number.tsx b/src/components/field/emtry-number.tsx index fc20e98..e637af8 100644 --- a/src/components/field/emtry-number.tsx +++ b/src/components/field/emtry-number.tsx @@ -3,7 +3,7 @@ import { useRef, useState } from "react"; import { Text, TextField, type TextFieldRef } from "@expo/ui/swift-ui"; import { frame } from "@expo/ui/swift-ui/modifiers"; -import { FIELD_LABELS } from "@/core/constants"; +import { FIELD_LABELS } from "@/core/constants/journal"; import { FieldWrapper } from "./field-wrapper"; diff --git a/src/components/field/entry-date.tsx b/src/components/field/entry-date.tsx index d784c25..ca6b22f 100644 --- a/src/components/field/entry-date.tsx +++ b/src/components/field/entry-date.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { DatePicker, Text } from "@expo/ui/swift-ui"; -import { FIELD_LABELS } from "@/core/constants"; +import { FIELD_LABELS } from "@/core/constants/journal"; import { formatDate } from "@/utils/date"; import { FieldWrapper } from "./field-wrapper"; diff --git a/src/components/field/entry-long-text.tsx b/src/components/field/entry-long-text.tsx index c47f052..5fc15a6 100644 --- a/src/components/field/entry-long-text.tsx +++ b/src/components/field/entry-long-text.tsx @@ -1,7 +1,7 @@ import { Text, TextField } from "@expo/ui/swift-ui"; import { frame, lineLimit } from "@expo/ui/swift-ui/modifiers"; -import { FIELD_LABELS } from "@/core/constants"; +import { FIELD_LABELS } from "@/core/constants/journal"; import { FieldWrapper } from "./field-wrapper"; diff --git a/src/components/field/entry-text.tsx b/src/components/field/entry-text.tsx index 5e382d4..f86a673 100644 --- a/src/components/field/entry-text.tsx +++ b/src/components/field/entry-text.tsx @@ -1,7 +1,7 @@ import { Text, TextField } from "@expo/ui/swift-ui"; import { frame } from "@expo/ui/swift-ui/modifiers"; -import { FIELD_LABELS } from "@/core/constants"; +import { FIELD_LABELS } from "@/core/constants/journal"; import { FieldWrapper } from "./field-wrapper"; diff --git a/src/components/field/entry-time.tsx b/src/components/field/entry-time.tsx index b567d6e..ca15502 100644 --- a/src/components/field/entry-time.tsx +++ b/src/components/field/entry-time.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { DatePicker, Text } from "@expo/ui/swift-ui"; -import { FIELD_LABELS } from "@/core/constants"; +import { FIELD_LABELS } from "@/core/constants/journal"; import { formatTime } from "@/utils/date"; import { FieldWrapper } from "./field-wrapper"; diff --git a/src/components/journal/field-bottom-sheet.tsx b/src/components/journal/field-bottom-sheet.tsx index d36a66a..c01cae9 100644 --- a/src/components/journal/field-bottom-sheet.tsx +++ b/src/components/journal/field-bottom-sheet.tsx @@ -10,7 +10,7 @@ import { scrollDisabled, } from "@expo/ui/swift-ui/modifiers"; -import { FIELD_ICONS, FIELD_LABELS, FieldType } from "@/core/constants"; +import { FIELD_ICONS, FIELD_LABELS, FieldType } from "@/core/constants/journal"; import { FIELD_TYPES } from "@/utils/journal/use-journal-field"; type Props = { diff --git a/src/components/journal/icon-select-bottom-sheet.tsx b/src/components/journal/icon-select-bottom-sheet.tsx index 5818588..7e38c8a 100644 --- a/src/components/journal/icon-select-bottom-sheet.tsx +++ b/src/components/journal/icon-select-bottom-sheet.tsx @@ -20,7 +20,7 @@ import { } from "@expo/ui/swift-ui/modifiers"; import { SFSymbol } from "expo-symbols"; -import { JOURNAL_ICONS } from "@/core/constants"; +import { JOURNAL_ICONS } from "@/core/constants/journal"; import { chunkArray } from "@/utils/chunk-array"; type Props = { diff --git a/src/components/journal/journal-create-view.tsx b/src/components/journal/journal-create-view.tsx index 71cb807..584e830 100644 --- a/src/components/journal/journal-create-view.tsx +++ b/src/components/journal/journal-create-view.tsx @@ -22,7 +22,7 @@ import { padding, } from "@expo/ui/swift-ui/modifiers"; -import { FIELD_ICONS, FIELD_LABELS, FieldType } from "@/core/constants"; +import { FIELD_ICONS, FIELD_LABELS, FieldType } from "@/core/constants/journal"; import { hexColorSchema } from "@/utils/journal/color"; import { JournalMetaObj, type FieldDraftObj } from "@/utils/journal/use-journal-field"; diff --git a/src/core/constants.ts b/src/core/constants/journal.ts similarity index 100% rename from src/core/constants.ts rename to src/core/constants/journal.ts diff --git a/src/db/schemas/fields.ts b/src/db/schemas/fields.ts index 54d1da6..d057edc 100644 --- a/src/db/schemas/fields.ts +++ b/src/db/schemas/fields.ts @@ -4,8 +4,8 @@ import { relations } from "drizzle-orm"; import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; import { createInsertSchema, createSelectSchema } from "drizzle-zod"; -import { fieldTypeSchema } from "@/core/constants"; -import type { FieldType } from "@/core/constants"; +import { fieldTypeSchema } from "@/core/constants/journal"; +import type { FieldType } from "@/core/constants/journal"; import { entryValues } from "./entries"; import { journals } from "./journals"; diff --git a/src/db/seed.ts b/src/db/seed.ts index 5e0f5b9..3a2e7a9 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -1,7 +1,7 @@ import { count } from "drizzle-orm"; import * as Crypto from "expo-crypto"; -import type { FieldType } from "@/core/constants"; +import type { FieldType } from "@/core/constants/journal"; import { db } from "./client"; import { entries, entryValues, fields, journals } from "./schemas"; diff --git a/src/utils/days/import-journal.ts b/src/utils/days/import-journal.ts index 936df22..42eac08 100644 --- a/src/utils/days/import-journal.ts +++ b/src/utils/days/import-journal.ts @@ -4,7 +4,7 @@ import * as DocumentPicker from "expo-document-picker"; import { File } from "expo-file-system"; import { z } from "zod"; -import { journalIconSchema } from "@/core/constants"; +import { journalIconSchema } from "@/core/constants/journal"; import type { JournalDetail } from "@/db/queries/journals"; import { fieldSelectSchema, journalSelectSchema } from "@/db/schemas"; import { generateSignature } from "@/utils/days/export-journal"; diff --git a/src/utils/entry/preview.ts b/src/utils/entry/preview.ts index 54ea4cb..1c569b0 100644 --- a/src/utils/entry/preview.ts +++ b/src/utils/entry/preview.ts @@ -1,4 +1,4 @@ -import { FieldType } from "@/core/constants"; +import { FieldType } from "@/core/constants/journal"; import { EntryDetailObj } from "@/db/queries/entries"; import { formatDate, formatTime } from "../date"; diff --git a/src/utils/entry/use-entry.ts b/src/utils/entry/use-entry.ts index e2d456a..d8992a4 100644 --- a/src/utils/entry/use-entry.ts +++ b/src/utils/entry/use-entry.ts @@ -3,7 +3,7 @@ import { useRef } from "react"; import * as Crypto from "expo-crypto"; import { z } from "zod"; -import type { FieldType } from "@/core/constants"; +import type { FieldType } from "@/core/constants/journal"; import { storeEntry, updateEntryValues } from "@/db/queries/entries"; import { entrySelectSchema, diff --git a/src/utils/journal/use-journal-field.ts b/src/utils/journal/use-journal-field.ts index ec71ec9..10c241a 100644 --- a/src/utils/journal/use-journal-field.ts +++ b/src/utils/journal/use-journal-field.ts @@ -8,7 +8,7 @@ import { type FieldType, journalIconSchema, JOURNAL_ICONS, -} from "@/core/constants"; +} from "@/core/constants/journal"; import { storeJournal, updateJournal as updateJournalQuery } from "@/db/queries/journals"; import { fieldInsertSchema, type JournalObj } from "@/db/schemas"; import { hexColorSchema } from "@/utils/journal/color"; From 88b7fcc35e5dc52893504a3e1e50a1effdc75fe1 Mon Sep 17 00:00:00 2001 From: 273* Date: Tue, 11 Aug 2026 17:10:42 +0900 Subject: [PATCH 05/32] =?UTF-8?q?refactor:=20=E5=AE=9A=E6=95=B0=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E7=A7=BB=E5=8B=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/field/emtry-number.tsx | 2 +- src/components/field/entry-date.tsx | 2 +- src/components/field/entry-long-text.tsx | 2 +- src/components/field/entry-text.tsx | 2 +- src/components/field/entry-time.tsx | 2 +- src/components/journal/field-bottom-sheet.tsx | 2 +- src/components/journal/icon-select-bottom-sheet.tsx | 2 +- src/components/journal/journal-create-view.tsx | 2 +- src/{core => }/constants/journal.ts | 0 src/db/schemas/fields.ts | 4 ++-- src/db/seed.ts | 2 +- src/utils/days/import-journal.ts | 2 +- src/utils/entry/preview.ts | 2 +- src/utils/entry/use-entry.ts | 2 +- src/utils/journal/use-journal-field.ts | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) rename src/{core => }/constants/journal.ts (100%) diff --git a/src/components/field/emtry-number.tsx b/src/components/field/emtry-number.tsx index e637af8..9998af2 100644 --- a/src/components/field/emtry-number.tsx +++ b/src/components/field/emtry-number.tsx @@ -3,7 +3,7 @@ import { useRef, useState } from "react"; import { Text, TextField, type TextFieldRef } from "@expo/ui/swift-ui"; import { frame } from "@expo/ui/swift-ui/modifiers"; -import { FIELD_LABELS } from "@/core/constants/journal"; +import { FIELD_LABELS } from "@/constants/journal"; import { FieldWrapper } from "./field-wrapper"; diff --git a/src/components/field/entry-date.tsx b/src/components/field/entry-date.tsx index ca6b22f..8e069f3 100644 --- a/src/components/field/entry-date.tsx +++ b/src/components/field/entry-date.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { DatePicker, Text } from "@expo/ui/swift-ui"; -import { FIELD_LABELS } from "@/core/constants/journal"; +import { FIELD_LABELS } from "@/constants/journal"; import { formatDate } from "@/utils/date"; import { FieldWrapper } from "./field-wrapper"; diff --git a/src/components/field/entry-long-text.tsx b/src/components/field/entry-long-text.tsx index 5fc15a6..63a4ef7 100644 --- a/src/components/field/entry-long-text.tsx +++ b/src/components/field/entry-long-text.tsx @@ -1,7 +1,7 @@ import { Text, TextField } from "@expo/ui/swift-ui"; import { frame, lineLimit } from "@expo/ui/swift-ui/modifiers"; -import { FIELD_LABELS } from "@/core/constants/journal"; +import { FIELD_LABELS } from "@/constants/journal"; import { FieldWrapper } from "./field-wrapper"; diff --git a/src/components/field/entry-text.tsx b/src/components/field/entry-text.tsx index f86a673..102728d 100644 --- a/src/components/field/entry-text.tsx +++ b/src/components/field/entry-text.tsx @@ -1,7 +1,7 @@ import { Text, TextField } from "@expo/ui/swift-ui"; import { frame } from "@expo/ui/swift-ui/modifiers"; -import { FIELD_LABELS } from "@/core/constants/journal"; +import { FIELD_LABELS } from "@/constants/journal"; import { FieldWrapper } from "./field-wrapper"; diff --git a/src/components/field/entry-time.tsx b/src/components/field/entry-time.tsx index ca15502..b121e4a 100644 --- a/src/components/field/entry-time.tsx +++ b/src/components/field/entry-time.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { DatePicker, Text } from "@expo/ui/swift-ui"; -import { FIELD_LABELS } from "@/core/constants/journal"; +import { FIELD_LABELS } from "@/constants/journal"; import { formatTime } from "@/utils/date"; import { FieldWrapper } from "./field-wrapper"; diff --git a/src/components/journal/field-bottom-sheet.tsx b/src/components/journal/field-bottom-sheet.tsx index c01cae9..e64e2bd 100644 --- a/src/components/journal/field-bottom-sheet.tsx +++ b/src/components/journal/field-bottom-sheet.tsx @@ -10,7 +10,7 @@ import { scrollDisabled, } from "@expo/ui/swift-ui/modifiers"; -import { FIELD_ICONS, FIELD_LABELS, FieldType } from "@/core/constants/journal"; +import { FIELD_ICONS, FIELD_LABELS, FieldType } from "@/constants/journal"; import { FIELD_TYPES } from "@/utils/journal/use-journal-field"; type Props = { diff --git a/src/components/journal/icon-select-bottom-sheet.tsx b/src/components/journal/icon-select-bottom-sheet.tsx index 7e38c8a..0e286b4 100644 --- a/src/components/journal/icon-select-bottom-sheet.tsx +++ b/src/components/journal/icon-select-bottom-sheet.tsx @@ -20,7 +20,7 @@ import { } from "@expo/ui/swift-ui/modifiers"; import { SFSymbol } from "expo-symbols"; -import { JOURNAL_ICONS } from "@/core/constants/journal"; +import { JOURNAL_ICONS } from "@/constants/journal"; import { chunkArray } from "@/utils/chunk-array"; type Props = { diff --git a/src/components/journal/journal-create-view.tsx b/src/components/journal/journal-create-view.tsx index 584e830..572ddb3 100644 --- a/src/components/journal/journal-create-view.tsx +++ b/src/components/journal/journal-create-view.tsx @@ -22,7 +22,7 @@ import { padding, } from "@expo/ui/swift-ui/modifiers"; -import { FIELD_ICONS, FIELD_LABELS, FieldType } from "@/core/constants/journal"; +import { FIELD_ICONS, FIELD_LABELS, FieldType } from "@/constants/journal"; import { hexColorSchema } from "@/utils/journal/color"; import { JournalMetaObj, type FieldDraftObj } from "@/utils/journal/use-journal-field"; diff --git a/src/core/constants/journal.ts b/src/constants/journal.ts similarity index 100% rename from src/core/constants/journal.ts rename to src/constants/journal.ts diff --git a/src/db/schemas/fields.ts b/src/db/schemas/fields.ts index d057edc..401000e 100644 --- a/src/db/schemas/fields.ts +++ b/src/db/schemas/fields.ts @@ -4,8 +4,8 @@ import { relations } from "drizzle-orm"; import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; import { createInsertSchema, createSelectSchema } from "drizzle-zod"; -import { fieldTypeSchema } from "@/core/constants/journal"; -import type { FieldType } from "@/core/constants/journal"; +import { fieldTypeSchema } from "@/constants/journal"; +import type { FieldType } from "@/constants/journal"; import { entryValues } from "./entries"; import { journals } from "./journals"; diff --git a/src/db/seed.ts b/src/db/seed.ts index 3a2e7a9..d470d05 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -1,7 +1,7 @@ import { count } from "drizzle-orm"; import * as Crypto from "expo-crypto"; -import type { FieldType } from "@/core/constants/journal"; +import type { FieldType } from "@/constants/journal"; import { db } from "./client"; import { entries, entryValues, fields, journals } from "./schemas"; diff --git a/src/utils/days/import-journal.ts b/src/utils/days/import-journal.ts index 42eac08..5fc6201 100644 --- a/src/utils/days/import-journal.ts +++ b/src/utils/days/import-journal.ts @@ -4,7 +4,7 @@ import * as DocumentPicker from "expo-document-picker"; import { File } from "expo-file-system"; import { z } from "zod"; -import { journalIconSchema } from "@/core/constants/journal"; +import { journalIconSchema } from "@/constants/journal"; import type { JournalDetail } from "@/db/queries/journals"; import { fieldSelectSchema, journalSelectSchema } from "@/db/schemas"; import { generateSignature } from "@/utils/days/export-journal"; diff --git a/src/utils/entry/preview.ts b/src/utils/entry/preview.ts index 1c569b0..2c072b2 100644 --- a/src/utils/entry/preview.ts +++ b/src/utils/entry/preview.ts @@ -1,4 +1,4 @@ -import { FieldType } from "@/core/constants/journal"; +import { FieldType } from "@/constants/journal"; import { EntryDetailObj } from "@/db/queries/entries"; import { formatDate, formatTime } from "../date"; diff --git a/src/utils/entry/use-entry.ts b/src/utils/entry/use-entry.ts index d8992a4..5780322 100644 --- a/src/utils/entry/use-entry.ts +++ b/src/utils/entry/use-entry.ts @@ -3,7 +3,7 @@ import { useRef } from "react"; import * as Crypto from "expo-crypto"; import { z } from "zod"; -import type { FieldType } from "@/core/constants/journal"; +import type { FieldType } from "@/constants/journal"; import { storeEntry, updateEntryValues } from "@/db/queries/entries"; import { entrySelectSchema, diff --git a/src/utils/journal/use-journal-field.ts b/src/utils/journal/use-journal-field.ts index 10c241a..27102d3 100644 --- a/src/utils/journal/use-journal-field.ts +++ b/src/utils/journal/use-journal-field.ts @@ -8,7 +8,7 @@ import { type FieldType, journalIconSchema, JOURNAL_ICONS, -} from "@/core/constants/journal"; +} from "@/constants/journal"; import { storeJournal, updateJournal as updateJournalQuery } from "@/db/queries/journals"; import { fieldInsertSchema, type JournalObj } from "@/db/schemas"; import { hexColorSchema } from "@/utils/journal/color"; From 818c133ad37fe99c0b284a43a271609209fe3a67 Mon Sep 17 00:00:00 2001 From: 273* Date: Wed, 12 Aug 2026 12:57:18 +0900 Subject: [PATCH 06/32] =?UTF-8?q?feat:=20llm=E3=82=92=E5=8B=95=E3=81=8B?= =?UTF-8?q?=E3=81=99=E4=BB=AEUI=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/_layout.tsx | 1 + src/components/days/days-llm-fb.tsx | 22 ++++++++++++++-- src/components/days/days-view.tsx | 5 +++- src/db/queries/entries.ts | 3 +++ src/polyfills.ts | 2 ++ .../days/reflection/get-reflection-model.ts | 25 +++++-------------- 6 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 src/polyfills.ts diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index 5d4f83c..a8b36e0 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -1,3 +1,4 @@ +import "@/polyfills"; import React from "react"; import AppTabs from "@/components/app-tabs"; diff --git a/src/components/days/days-llm-fb.tsx b/src/components/days/days-llm-fb.tsx index 5168098..fc1d994 100644 --- a/src/components/days/days-llm-fb.tsx +++ b/src/components/days/days-llm-fb.tsx @@ -1,14 +1,29 @@ import { PlatformColor } from "react-native"; -import { HStack, Image, Section, Text } from "@expo/ui/swift-ui"; +import { Button, HStack, Image, Section, Spacer, Text } from "@expo/ui/swift-ui"; import { font, foregroundStyle } from "@expo/ui/swift-ui/modifiers"; +import { DailyEntryObj } from "@/db/queries/entries"; +import { getReflectionCategory } from "@/utils/days/reflection/get-reflection-category"; + import { FieldWrapper } from "../field/field-wrapper"; +type Props = { + /** + * 日毎のエントリー + */ + entries: DailyEntryObj[]; +}; + /** * LLM によるフィードバックを表示する */ -export function DaysLLMFB() { +export function DaysLLMFB({ entries }: Props) { + const handleGenerateReflection = async () => { + const result = await getReflectionCategory(entries); + console.log(result); + }; + return (
{`Today's reflection`} + + +
+ <> + + + ); } diff --git a/src/components/settings/system.tsx b/src/components/settings/system.tsx deleted file mode 100644 index 626cc23..0000000 --- a/src/components/settings/system.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { HStack, Section, Spacer, Text } from "@expo/ui/swift-ui"; -import { foregroundStyle } from "@expo/ui/swift-ui/modifiers"; - -export function System() { - return ( -
- - Language - - - 日本語 - - - - 外観 - - - システム - - -
- ); -} From ace86a6aa7a50163f591bb80a2a1204672fa54fe Mon Sep 17 00:00:00 2001 From: 273* Date: Fri, 14 Aug 2026 21:04:10 +0900 Subject: [PATCH 21/32] =?UTF-8?q?refactor:=20=E3=82=B3=E3=83=B3=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E5=90=8D=E3=82=92=E3=83=AA?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 4 ---- .../days/{days-llm-fb.tsx => days-llm-reflection.tsx} | 2 +- src/components/days/days-view.tsx | 4 ++-- 3 files changed, 3 insertions(+), 7 deletions(-) rename src/components/days/{days-llm-fb.tsx => days-llm-reflection.tsx} (97%) diff --git a/.env.example b/.env.example index 8acdd41..276ba34 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1 @@ EXPO_PUBLIC_SIGNING_SECRET= - -# Download model from HuggingFace -# https://huggingface.co/ggml-org/gemma-3-4b-it-GGUF -EXPO_PUBLIC_REFLECTION_LLM=ggml-org/gemma-3-4b-it-GGUF/gemma-3-4b-it-Q4_K_M.gguf diff --git a/src/components/days/days-llm-fb.tsx b/src/components/days/days-llm-reflection.tsx similarity index 97% rename from src/components/days/days-llm-fb.tsx rename to src/components/days/days-llm-reflection.tsx index c6fc971..965758e 100644 --- a/src/components/days/days-llm-fb.tsx +++ b/src/components/days/days-llm-reflection.tsx @@ -21,7 +21,7 @@ type Props = { /** * AI Reflection を表示する */ -export function DaysLLMFB({ entries }: Props) { +export function DaysLLMReflection({ entries }: Props) { const { aiReflectionEnabled, aiModel } = useAIReflectionSettings(); const [reflection, setReflection] = useState(); diff --git a/src/components/days/days-view.tsx b/src/components/days/days-view.tsx index 358fec6..a65a5eb 100644 --- a/src/components/days/days-view.tsx +++ b/src/components/days/days-view.tsx @@ -8,7 +8,7 @@ import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { getEntriesByDateQuery } from "@/db/queries/entries"; import { DaysCard } from "./days-card"; -import { DaysLLMFB } from "./days-llm-fb"; +import { DaysLLMReflection } from "./days-llm-reflection"; type Props = { /** @@ -59,7 +59,7 @@ export function DaysView({ date }: Props) { animation(Animation.default, expandedIds.size), ]} > - + {entries.map((entry, index) => ( Date: Sun, 16 Aug 2026 15:25:12 +0900 Subject: [PATCH 22/32] =?UTF-8?q?fix:=20bookmark=E3=81=AE=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/entry/[id].tsx | 16 +++++++++++++--- src/app/(journal)/index.tsx | 2 +- src/components/entry/entry-detail.tsx | 18 ++---------------- src/components/entry/entry-row.tsx | 2 +- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/app/(journal)/entry/[id].tsx b/src/app/(journal)/entry/[id].tsx index 7e189c1..cf95c8c 100644 --- a/src/app/(journal)/entry/[id].tsx +++ b/src/app/(journal)/entry/[id].tsx @@ -84,10 +84,10 @@ export default function EntryDetailScreen() { type: "action", icon: { type: "sfSymbol", - name: entry?.bookmark ? "bookmark.slash" : "bookmark", + name: "square.and.arrow.up", }, - label: entry?.bookmark ? "Unbookmark" : "Bookmark", - onPress: bookmark, + label: "Export Entry", + onPress: () => console.log("Export Entry"), }, { type: "action", @@ -99,6 +99,16 @@ export default function EntryDetailScreen() { ], }, }, + { + type: "button", + icon: { + type: "sfSymbol", + name: entry?.bookmark ? "bookmark.fill" : "bookmark", + }, + label: entry?.bookmark ? "Unbookmark" : "Bookmark", + tintColor: entry?.bookmark ? PlatformColor("systemIndigo") : undefined, + onPress: bookmark, + }, { type: "button", label: "Edit", diff --git a/src/app/(journal)/index.tsx b/src/app/(journal)/index.tsx index ead0df1..ec4a7e7 100644 --- a/src/app/(journal)/index.tsx +++ b/src/app/(journal)/index.tsx @@ -64,7 +64,7 @@ export default function JournalScreen() { type: "action" as const, icon: { type: "sfSymbol" as const, - name: bookmarkOnly ? ("bookmark.fill" as const) : ("bookmark" as const), + name: bookmarkOnly ? ("bookmark" as const) : ("bookmark.fill" as const), }, label: bookmarkOnly ? "Show All" : "Bookmarked Only", onPress: () => setBookmarkOnly((prev) => !prev), diff --git a/src/components/entry/entry-detail.tsx b/src/components/entry/entry-detail.tsx index ce700a7..27f1ced 100644 --- a/src/components/entry/entry-detail.tsx +++ b/src/components/entry/entry-detail.tsx @@ -1,6 +1,6 @@ import { PlatformColor, View } from "react-native"; -import { Host, HStack, Image, List, Section, Spacer, Text } from "@expo/ui/swift-ui"; +import { Host, List, Section, Text } from "@expo/ui/swift-ui"; import { frame, listStyle } from "@expo/ui/swift-ui/modifiers"; import { EntryDetailObj } from "@/db/queries/entries"; @@ -30,21 +30,7 @@ export function EntryDetailView({ entry }: Props) { useViewportSizeMeasurement > -
- {formatDate(entry.createdAt)} - - {entry.bookmark && ( - - )} - - } - > +
{formatDate(entry.createdAt)}}> {sorted.map((v) => (
+ ); } diff --git a/src/components/days/days-view.tsx b/src/components/days/days-view.tsx index 280125f..f46d0ba 100644 --- a/src/components/days/days-view.tsx +++ b/src/components/days/days-view.tsx @@ -59,7 +59,7 @@ export function DaysView({ date }: Props) { animation(Animation.default, expandedIds.size), ]} > - + {entries.map((entry, index) => ( { + const now = new Date(); + return ( + now.getHours() > reflectionTime.getHours() || + (now.getHours() === reflectionTime.getHours() && + now.getMinutes() >= reflectionTime.getMinutes()) + ); +}; + +/** + * アプリ起動時に今日の AI Reflection を自動生成するフック + * + * 以下の条件をすべて満たす場合に生成: + * 1. aiReflectionEnabled が true + * 2. 現在時刻が reflectionTime を過ぎている + * 3. 今日の reflection が DB に未保存 + * 4. 今日のエントリーが1件以上ある + */ +export function useAutoReflection() { + console.log("生成!"); + const { aiReflectionEnabled, aiModel, reflectionTime } = useAIReflectionSettings(); + + const today = useMemo(() => { + const d = new Date(); + d.setHours(0, 0, 0, 0); + return d; + }, []); + + const { data: entries } = useLiveQuery(getEntriesByDateQuery(today), [today.getTime()]); + const { data: reflection } = useLiveQuery(getReflectionByDateQuery(today), [today.getTime()]); + const generating = useRef(false); + + useEffect(() => { + if (!aiReflectionEnabled) return; + if (reflection) return; + if (!entries || entries.length === 0) return; + if (!isPastReflectionTime(reflectionTime)) return; + if (generating.current) return; + + generating.current = true; + getReflection(entries, aiModel.gguf) + .then((result) => result && storeReflection(today, result)) + .catch((error) => console.warn("[auto-reflection]", error)) + .finally(() => { + generating.current = false; + }); + }, [entries, reflection, aiReflectionEnabled, reflectionTime, aiModel.gguf, today]); +} From bef1ef3cb7a5fe6c1f392c41f1f123f68f6947c6 Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 16 Aug 2026 19:44:23 +0900 Subject: [PATCH 25/32] =?UTF-8?q?refactor:=20=E3=82=BF=E3=82=A4=E3=83=A0?= =?UTF-8?q?=E3=82=B9=E3=82=BF=E3=83=B3=E3=83=97=E5=91=A8=E3=82=8A=E3=81=AE?= =?UTF-8?q?=E9=96=A2=E6=95=B0=E3=82=92=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/days/index.tsx | 11 +++-------- src/db/queries/entries.ts | 8 +++----- src/db/queries/reflections.ts | 14 ++++---------- src/hooks/use-auto-reflection.ts | 23 +++++++++++------------ src/utils/date.ts | 23 +++++++++++++++++++---- 5 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/app/days/index.tsx b/src/app/days/index.tsx index 913ef1d..47ae70f 100644 --- a/src/app/days/index.tsx +++ b/src/app/days/index.tsx @@ -21,7 +21,7 @@ import { GlassView } from "expo-glass-effect"; import { Stack, useRouter } from "expo-router"; import { DaysView } from "@/components/days/days-view"; -import { addDays, formatDateDays } from "@/utils/date"; +import { addDays, formatDateDays, startOfDay } from "@/utils/date"; const SWIPE_THRESHOLD = 50; @@ -30,11 +30,7 @@ const SWIPE_THRESHOLD = 50; */ export default function DaysScreen() { const { width: screenWidth } = useWindowDimensions(); - const [selectedDate, setSelectedDate] = useState(() => { - const d = new Date(); - d.setHours(0, 0, 0, 0); - return d; - }); + const [selectedDate, setSelectedDate] = useState(() => startOfDay()); const [showCalendar, setShowCalendar] = useState(false); const router = useRouter(); @@ -45,8 +41,7 @@ export default function DaysScreen() { const prevDate = addDays(selectedDate, -1); const nextDate = addDays(selectedDate, 1); - const today = new Date(); - today.setHours(0, 0, 0, 0); + const today = startOfDay(); const canGoNext = nextDate <= today; diff --git a/src/db/queries/entries.ts b/src/db/queries/entries.ts index bf5dbc8..55f9c9a 100644 --- a/src/db/queries/entries.ts +++ b/src/db/queries/entries.ts @@ -1,6 +1,7 @@ import { and, eq, gte, lt } from "drizzle-orm"; import { db } from "@/db/client"; +import { addDays, startOfDay } from "@/utils/date"; import { entries, EntryObj, EntryValueObj, entryValues } from "../schemas"; @@ -101,11 +102,8 @@ export const deleteAllEntries = async (journalId?: string) => { * @param date 対象日(時刻は無視される) */ export const getEntriesByDateQuery = (date: Date) => { - const start = new Date(date); - start.setHours(0, 0, 0, 0); - - const end = new Date(start); - end.setDate(end.getDate() + 1); + const start = startOfDay(date); + const end = addDays(start, 1); return db.query.entries.findMany({ where: and(gte(entries.createdAt, start.getTime()), lt(entries.createdAt, end.getTime())), diff --git a/src/db/queries/reflections.ts b/src/db/queries/reflections.ts index 18e30f7..be57fd6 100644 --- a/src/db/queries/reflections.ts +++ b/src/db/queries/reflections.ts @@ -4,20 +4,14 @@ import * as Crypto from "expo-crypto"; import type { ReflectionResult } from "@/constants/reflection"; import { db } from "@/db/client"; import { reflections } from "@/db/schemas"; - -/** 日付を 00:00:00 のタイムスタンプに正規化 */ -const normalizeDate = (date: Date) => { - const d = new Date(date); - d.setHours(0, 0, 0, 0); - return d.getTime(); -}; +import { startOfDayTimestamp } from "@/utils/date"; /** * 指定日の振り返りを取得するクエリ(useLiveQuery 用) */ export const getReflectionByDateQuery = (date: Date) => db.query.reflections.findFirst({ - where: eq(reflections.date, normalizeDate(date)), + where: eq(reflections.date, startOfDayTimestamp(date)), }); /** @@ -29,7 +23,7 @@ export const storeReflection = async (date: Date, result: ReflectionResult): Pro .insert(reflections) .values({ id: Crypto.randomUUID(), - date: normalizeDate(date), + date: startOfDayTimestamp(date), title: result.title, firstCategory: result.items[0].category, firstContent: result.items[0].content, @@ -54,6 +48,6 @@ export const storeReflection = async (date: Date, result: ReflectionResult): Pro */ export const deleteReflection = async (date: Date): Promise => { await db.transaction(async (tx) => { - await tx.delete(reflections).where(eq(reflections.date, normalizeDate(date))); + await tx.delete(reflections).where(eq(reflections.date, startOfDayTimestamp(date))); }); }; diff --git a/src/hooks/use-auto-reflection.ts b/src/hooks/use-auto-reflection.ts index 9d88cb5..c1324de 100644 --- a/src/hooks/use-auto-reflection.ts +++ b/src/hooks/use-auto-reflection.ts @@ -4,6 +4,7 @@ import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { getEntriesByDateQuery } from "@/db/queries/entries"; import { getReflectionByDateQuery, storeReflection } from "@/db/queries/reflections"; +import { startOfDay } from "@/utils/date"; import { getReflection } from "@/utils/days/reflection/get-reflection"; import { useAIReflectionSettings } from "@/utils/settings/use-ai-reflection-settings"; @@ -27,14 +28,8 @@ const isPastReflectionTime = (reflectionTime: Date): boolean => { * 4. 今日のエントリーが1件以上ある */ export function useAutoReflection() { - console.log("生成!"); const { aiReflectionEnabled, aiModel, reflectionTime } = useAIReflectionSettings(); - - const today = useMemo(() => { - const d = new Date(); - d.setHours(0, 0, 0, 0); - return d; - }, []); + const today = useMemo(() => startOfDay(), []); const { data: entries } = useLiveQuery(getEntriesByDateQuery(today), [today.getTime()]); const { data: reflection } = useLiveQuery(getReflectionByDateQuery(today), [today.getTime()]); @@ -48,11 +43,15 @@ export function useAutoReflection() { if (generating.current) return; generating.current = true; - getReflection(entries, aiModel.gguf) - .then((result) => result && storeReflection(today, result)) - .catch((error) => console.warn("[auto-reflection]", error)) - .finally(() => { + (async () => { + try { + const result = await getReflection(entries, aiModel.gguf); + if (result) await storeReflection(today, result); + } catch (error) { + console.warn("[auto-reflection]", error); + } finally { generating.current = false; - }); + } + })(); }, [entries, reflection, aiReflectionEnabled, reflectionTime, aiModel.gguf, today]); } diff --git a/src/utils/date.ts b/src/utils/date.ts index 932af46..e051e17 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -35,6 +35,24 @@ export const formatDateDays = (date: Date) => ? date.toLocaleDateString("en-US", { month: "long", day: "numeric" }) : date.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); +/** + * 日付を 00:00:00.000 に正規化した Date を返す + * @param date 基準日付(省略時は今日) + * @returns 正規化した日付 + */ +export const startOfDay = (date: Date = new Date()): Date => { + const d = new Date(date); + d.setHours(0, 0, 0, 0); + return d; +}; + +/** + * 日付を 00:00:00.000 に正規化したタイムスタンプを返す + * @param date 基準日付 + * @returns 正規化したタイムスタンプ + */ +export const startOfDayTimestamp = (date: Date): number => startOfDay(date).getTime(); + /** 日付を n 日ずらす * @param date 基準日付 * @param days 日数 @@ -42,9 +60,6 @@ export const formatDateDays = (date: Date) => */ export const addDays = (date: Date, days: number) => { const d = new Date(date); - d.setDate(d.getDate() + days); - d.setHours(0, 0, 0, 0); - - return d; + return startOfDay(d); }; From 85b03c999b8862439d4163639d24aeee3f7e647e Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 16 Aug 2026 19:48:41 +0900 Subject: [PATCH 26/32] =?UTF-8?q?refactor:=20=E3=82=AB=E3=82=B9=E3=82=BF?= =?UTF-8?q?=E3=83=A0=E3=83=95=E3=83=83=E3=82=AF=E5=91=A8=E3=82=8A=E3=81=AE?= =?UTF-8?q?=E9=96=A2=E6=95=B0=E3=82=92=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/create.tsx | 2 +- src/app/(journal)/edit.tsx | 2 +- src/app/(journal)/entry/[id].tsx | 2 +- src/app/(journal)/entry/create.tsx | 2 +- src/components/days/days-card.tsx | 2 +- src/components/days/days-llm-reflection.tsx | 2 +- src/components/entry/entry-create-view.tsx | 2 +- src/components/entry/entry-detail.tsx | 2 +- src/components/entry/entry-field-item.tsx | 2 +- src/components/entry/entry-list-view.tsx | 2 +- src/components/journal/field-bottom-sheet.tsx | 2 +- src/components/journal/journal-create-view.tsx | 2 +- src/components/settings/application.tsx | 2 +- src/db/queries/journals.ts | 2 +- src/{utils => hooks}/entry/use-entry-detail.ts | 2 +- src/{utils => hooks}/entry/use-entry-list.ts | 3 +-- src/{utils => hooks}/entry/use-entry.ts | 0 src/{utils => hooks}/journal/use-journal-field.ts | 0 src/{utils => hooks}/settings/use-ai-reflection-settings.ts | 0 src/hooks/{ => settings}/use-auto-reflection.ts | 2 +- src/utils/entry/entry-form.ts | 3 +-- 21 files changed, 18 insertions(+), 20 deletions(-) rename src/{utils => hooks}/entry/use-entry-detail.ts (94%) rename src/{utils => hooks}/entry/use-entry-list.ts (93%) rename src/{utils => hooks}/entry/use-entry.ts (100%) rename src/{utils => hooks}/journal/use-journal-field.ts (100%) rename src/{utils => hooks}/settings/use-ai-reflection-settings.ts (100%) rename src/hooks/{ => settings}/use-auto-reflection.ts (97%) diff --git a/src/app/(journal)/create.tsx b/src/app/(journal)/create.tsx index dd8242d..2f081b4 100644 --- a/src/app/(journal)/create.tsx +++ b/src/app/(journal)/create.tsx @@ -6,9 +6,9 @@ import { Stack, useRouter } from "expo-router"; import { z } from "zod"; import { JournalCreateView } from "@/components/journal/journal-create-view"; +import { useJournalField } from "@/hooks/journal/use-journal-field"; import { importJournal } from "@/utils/days/import-journal"; import { setCreatedJournalId } from "@/utils/journal/created-journal"; -import { useJournalField } from "@/utils/journal/use-journal-field"; /** * ジャーナル作成 diff --git a/src/app/(journal)/edit.tsx b/src/app/(journal)/edit.tsx index 76b18f2..5fb5166 100644 --- a/src/app/(journal)/edit.tsx +++ b/src/app/(journal)/edit.tsx @@ -9,8 +9,8 @@ import { z } from "zod"; import { JournalCreateView } from "@/components/journal/journal-create-view"; import { deleteJournal, getJournalDetailQuery, JournalDetail } from "@/db/queries/journals"; import { FieldObj } from "@/db/schemas"; +import { FieldDraftObj, useJournalField } from "@/hooks/journal/use-journal-field"; import { exportJournal } from "@/utils/days/export-journal"; -import { FieldDraftObj, useJournalField } from "@/utils/journal/use-journal-field"; import { useValidatedParams } from "@/utils/params"; type FormProps = { diff --git a/src/app/(journal)/entry/[id].tsx b/src/app/(journal)/entry/[id].tsx index cf95c8c..6d7f89f 100644 --- a/src/app/(journal)/entry/[id].tsx +++ b/src/app/(journal)/entry/[id].tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import { EntryCreateView } from "@/components/entry/entry-create-view"; import { EntryDetailView } from "@/components/entry/entry-detail"; -import { useEntryDetail } from "@/utils/entry/use-entry-detail"; +import { useEntryDetail } from "@/hooks/entry/use-entry-detail"; import { useValidatedParams } from "@/utils/params"; /** diff --git a/src/app/(journal)/entry/create.tsx b/src/app/(journal)/entry/create.tsx index c090ee5..fb35e1d 100644 --- a/src/app/(journal)/entry/create.tsx +++ b/src/app/(journal)/entry/create.tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import { EntryCreateView } from "@/components/entry/entry-create-view"; import { getFieldsQuery } from "@/db/queries/fields"; -import { useEntry } from "@/utils/entry/use-entry"; +import { useEntry } from "@/hooks/entry/use-entry"; import { useValidatedParams } from "@/utils/params"; /** diff --git a/src/components/days/days-card.tsx b/src/components/days/days-card.tsx index b2fff1b..0c58e03 100644 --- a/src/components/days/days-card.tsx +++ b/src/components/days/days-card.tsx @@ -5,8 +5,8 @@ import { font, foregroundStyle, listRowSeparator, opacity } from "@expo/ui/swift import { EntryDetailObj } from "@/db/queries/entries"; import { JournalObj } from "@/db/schemas"; +import { deserializeValue } from "@/hooks/entry/use-entry"; import { formatTime } from "@/utils/date"; -import { deserializeValue } from "@/utils/entry/use-entry"; import { EntryFieldItem } from "../entry/entry-field-item"; diff --git a/src/components/days/days-llm-reflection.tsx b/src/components/days/days-llm-reflection.tsx index 2b1268b..90d5699 100644 --- a/src/components/days/days-llm-reflection.tsx +++ b/src/components/days/days-llm-reflection.tsx @@ -5,7 +5,7 @@ import { font, foregroundStyle } from "@expo/ui/swift-ui/modifiers"; import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { getReflectionByDateQuery } from "@/db/queries/reflections"; -import { useAIReflectionSettings } from "@/utils/settings/use-ai-reflection-settings"; +import { useAIReflectionSettings } from "@/hooks/settings/use-ai-reflection-settings"; import { FieldWrapper } from "../field/field-wrapper"; diff --git a/src/components/entry/entry-create-view.tsx b/src/components/entry/entry-create-view.tsx index 78c21b2..ea68137 100644 --- a/src/components/entry/entry-create-view.tsx +++ b/src/components/entry/entry-create-view.tsx @@ -5,8 +5,8 @@ import { frame, listStyle } from "@expo/ui/swift-ui/modifiers"; import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { getFieldsQuery } from "@/db/queries/fields"; +import { FieldValue } from "@/hooks/entry/use-entry"; import { formatDate } from "@/utils/date"; -import { FieldValue } from "@/utils/entry/use-entry"; import { EntryFieldItem } from "./entry-field-item"; diff --git a/src/components/entry/entry-detail.tsx b/src/components/entry/entry-detail.tsx index 27f1ced..544926e 100644 --- a/src/components/entry/entry-detail.tsx +++ b/src/components/entry/entry-detail.tsx @@ -4,8 +4,8 @@ import { Host, List, Section, Text } from "@expo/ui/swift-ui"; import { frame, listStyle } from "@expo/ui/swift-ui/modifiers"; import { EntryDetailObj } from "@/db/queries/entries"; +import { deserializeValue } from "@/hooks/entry/use-entry"; import { formatDate } from "@/utils/date"; -import { deserializeValue } from "@/utils/entry/use-entry"; import { EntryFieldItem } from "./entry-field-item"; diff --git a/src/components/entry/entry-field-item.tsx b/src/components/entry/entry-field-item.tsx index c29183e..80cfe9e 100644 --- a/src/components/entry/entry-field-item.tsx +++ b/src/components/entry/entry-field-item.tsx @@ -7,7 +7,7 @@ import { EntryMedia } from "@/components/field/entry-media"; import { EntryText } from "@/components/field/entry-text"; import { EntryTime } from "@/components/field/entry-time"; import type { FieldObj } from "@/db/schemas"; -import { type FieldValue } from "@/utils/entry/use-entry"; +import { type FieldValue } from "@/hooks/entry/use-entry"; type Props = { /** フィールド定義 */ diff --git a/src/components/entry/entry-list-view.tsx b/src/components/entry/entry-list-view.tsx index 76be9d3..d823974 100644 --- a/src/components/entry/entry-list-view.tsx +++ b/src/components/entry/entry-list-view.tsx @@ -9,7 +9,7 @@ import { SymbolView } from "expo-symbols"; import { JournalChipList } from "@/components/journal/journal-chip"; import { deleteEntry } from "@/db/queries/entries"; import { type JournalWithCountObj } from "@/db/queries/journals"; -import { useEntryList } from "@/utils/entry/use-entry-list"; +import { useEntryList } from "@/hooks/entry/use-entry-list"; import { EntryRow } from "./entry-row"; diff --git a/src/components/journal/field-bottom-sheet.tsx b/src/components/journal/field-bottom-sheet.tsx index e64e2bd..e10052f 100644 --- a/src/components/journal/field-bottom-sheet.tsx +++ b/src/components/journal/field-bottom-sheet.tsx @@ -11,7 +11,7 @@ import { } from "@expo/ui/swift-ui/modifiers"; import { FIELD_ICONS, FIELD_LABELS, FieldType } from "@/constants/journal"; -import { FIELD_TYPES } from "@/utils/journal/use-journal-field"; +import { FIELD_TYPES } from "@/hooks/journal/use-journal-field"; type Props = { /** ボトムシートの表示状態 */ diff --git a/src/components/journal/journal-create-view.tsx b/src/components/journal/journal-create-view.tsx index 572ddb3..6e5cb92 100644 --- a/src/components/journal/journal-create-view.tsx +++ b/src/components/journal/journal-create-view.tsx @@ -23,8 +23,8 @@ import { } from "@expo/ui/swift-ui/modifiers"; import { FIELD_ICONS, FIELD_LABELS, FieldType } from "@/constants/journal"; +import { JournalMetaObj, type FieldDraftObj } from "@/hooks/journal/use-journal-field"; import { hexColorSchema } from "@/utils/journal/color"; -import { JournalMetaObj, type FieldDraftObj } from "@/utils/journal/use-journal-field"; import { FieldBottomSheet } from "./field-bottom-sheet"; import { IconSelectBottomSheet } from "./icon-select-bottom-sheet"; diff --git a/src/components/settings/application.tsx b/src/components/settings/application.tsx index abe6048..3086992 100644 --- a/src/components/settings/application.tsx +++ b/src/components/settings/application.tsx @@ -6,7 +6,7 @@ import { disabled, tag, tint } from "@expo/ui/swift-ui/modifiers"; import { downloadModel, removeModel } from "@react-native-ai/llama"; import { AI_MODELS, type AIModelId } from "@/constants/ai-models"; -import { useAIReflectionSettings } from "@/utils/settings/use-ai-reflection-settings"; +import { useAIReflectionSettings } from "@/hooks/settings/use-ai-reflection-settings"; /** * アプリの機能設定 diff --git a/src/db/queries/journals.ts b/src/db/queries/journals.ts index 697bb20..c1525d4 100644 --- a/src/db/queries/journals.ts +++ b/src/db/queries/journals.ts @@ -2,7 +2,7 @@ import { and, eq, notInArray, sql } from "drizzle-orm"; import { db } from "@/db/client"; import { entries, fields, JournalObj, journals } from "@/db/schemas"; -import { FieldWithSortObj, JournalMetaObj } from "@/utils/journal/use-journal-field"; +import { FieldWithSortObj, JournalMetaObj } from "@/hooks/journal/use-journal-field"; /** * ジャーナル一覧を取得するクエリ diff --git a/src/utils/entry/use-entry-detail.ts b/src/hooks/entry/use-entry-detail.ts similarity index 94% rename from src/utils/entry/use-entry-detail.ts rename to src/hooks/entry/use-entry-detail.ts index 44d9e15..8057ef5 100644 --- a/src/utils/entry/use-entry-detail.ts +++ b/src/hooks/entry/use-entry-detail.ts @@ -3,8 +3,8 @@ import { Keyboard } from "react-native"; import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { bookmarkEntry, deleteEntry, getEntryDetailQuery } from "@/db/queries/entries"; +import { buildEntryFormData } from "@/utils/entry/entry-form"; -import { buildEntryFormData } from "./entry-form"; import { useEntry } from "./use-entry"; /** diff --git a/src/utils/entry/use-entry-list.ts b/src/hooks/entry/use-entry-list.ts similarity index 93% rename from src/utils/entry/use-entry-list.ts rename to src/hooks/entry/use-entry-list.ts index 2831391..682a828 100644 --- a/src/utils/entry/use-entry-list.ts +++ b/src/hooks/entry/use-entry-list.ts @@ -1,8 +1,7 @@ import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { getEntriesQuery } from "@/db/queries/entries"; - -import { buildPreviewEntry } from "./preview"; +import { buildPreviewEntry } from "@/utils/entry/preview"; type Params = { /** ジャーナル id */ diff --git a/src/utils/entry/use-entry.ts b/src/hooks/entry/use-entry.ts similarity index 100% rename from src/utils/entry/use-entry.ts rename to src/hooks/entry/use-entry.ts diff --git a/src/utils/journal/use-journal-field.ts b/src/hooks/journal/use-journal-field.ts similarity index 100% rename from src/utils/journal/use-journal-field.ts rename to src/hooks/journal/use-journal-field.ts diff --git a/src/utils/settings/use-ai-reflection-settings.ts b/src/hooks/settings/use-ai-reflection-settings.ts similarity index 100% rename from src/utils/settings/use-ai-reflection-settings.ts rename to src/hooks/settings/use-ai-reflection-settings.ts diff --git a/src/hooks/use-auto-reflection.ts b/src/hooks/settings/use-auto-reflection.ts similarity index 97% rename from src/hooks/use-auto-reflection.ts rename to src/hooks/settings/use-auto-reflection.ts index c1324de..3570f1d 100644 --- a/src/hooks/use-auto-reflection.ts +++ b/src/hooks/settings/use-auto-reflection.ts @@ -4,9 +4,9 @@ import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { getEntriesByDateQuery } from "@/db/queries/entries"; import { getReflectionByDateQuery, storeReflection } from "@/db/queries/reflections"; +import { useAIReflectionSettings } from "@/hooks/settings/use-ai-reflection-settings"; import { startOfDay } from "@/utils/date"; import { getReflection } from "@/utils/days/reflection/get-reflection"; -import { useAIReflectionSettings } from "@/utils/settings/use-ai-reflection-settings"; /** 現在時刻が reflectionTime を過ぎているか */ const isPastReflectionTime = (reflectionTime: Date): boolean => { diff --git a/src/utils/entry/entry-form.ts b/src/utils/entry/entry-form.ts index 95df779..5ac85fd 100644 --- a/src/utils/entry/entry-form.ts +++ b/src/utils/entry/entry-form.ts @@ -1,7 +1,6 @@ import { EntryDetailObj } from "@/db/queries/entries"; import { FieldObj } from "@/db/schemas"; - -import { deserializeValue, FieldValue } from "./use-entry"; +import { deserializeValue, FieldValue } from "@/hooks/entry/use-entry"; /** * エントリー詳細からフォーム用のフィールド一覧と初期値を導出する From f412b1c9591896cc65960e698f2eb097e8ea937f Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 16 Aug 2026 19:57:30 +0900 Subject: [PATCH 27/32] =?UTF-8?q?refactor:=20=E3=82=AB=E3=82=B9=E3=82=BF?= =?UTF-8?q?=E3=83=A0=E3=83=95=E3=83=83=E3=82=AF=E5=86=85=E3=81=AE=E9=96=A2?= =?UTF-8?q?=E6=95=B0=E3=82=92=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/_layout.tsx | 2 +- src/hooks/entry/use-entry-detail.ts | 4 +- src/hooks/entry/use-entry-list.ts | 4 +- src/hooks/entry/use-entry.ts | 55 ++--------------------- src/hooks/journal/use-journal-field.ts | 53 +++++++--------------- src/hooks/settings/use-auto-reflection.ts | 18 ++------ src/utils/date.ts | 12 +++++ src/utils/entry/entry-form.ts | 2 +- src/utils/entry/field-value.ts | 55 +++++++++++++++++++++++ src/utils/journal/journal-field.ts | 35 +++++++++++++++ 10 files changed, 131 insertions(+), 109 deletions(-) create mode 100644 src/utils/entry/field-value.ts create mode 100644 src/utils/journal/journal-field.ts diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index 29a9a0c..f69d5eb 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -3,7 +3,7 @@ import React from "react"; import AppTabs from "@/components/app-tabs"; import { DrizzleProvider } from "@/components/drizzle-provider"; -import { useAutoReflection } from "@/hooks/use-auto-reflection"; +import { useAutoReflection } from "@/hooks/settings/use-auto-reflection"; function AppContent() { useAutoReflection(); diff --git a/src/hooks/entry/use-entry-detail.ts b/src/hooks/entry/use-entry-detail.ts index 8057ef5..eb6ece9 100644 --- a/src/hooks/entry/use-entry-detail.ts +++ b/src/hooks/entry/use-entry-detail.ts @@ -11,7 +11,7 @@ import { useEntry } from "./use-entry"; * エントリー詳細画面のデータ取得・フォーム状態・アクションをまとめたフック * @param entryId エントリー id */ -export function useEntryDetail(entryId: string) { +export const useEntryDetail = (entryId: string) => { const { data: entry } = useLiveQuery(getEntryDetailQuery(entryId), [entryId]); const { fields, initialValues } = entry @@ -37,4 +37,4 @@ export function useEntryDetail(entryId: string) { }; return { entry, valuesRef, setValue, save, bookmark, remove }; -} +}; diff --git a/src/hooks/entry/use-entry-list.ts b/src/hooks/entry/use-entry-list.ts index 682a828..47ef9da 100644 --- a/src/hooks/entry/use-entry-list.ts +++ b/src/hooks/entry/use-entry-list.ts @@ -13,7 +13,7 @@ type Params = { /** * エントリー一覧を取得し、フィルターまで行うフック */ -export function useEntryList({ journalId, bookmarkOnly = false }: Params) { +export const useEntryList = ({ journalId, bookmarkOnly = false }: Params) => { const { data: entries } = useLiveQuery(getEntriesQuery(journalId), [journalId]); const previewEntries = entries.map(buildPreviewEntry); @@ -24,4 +24,4 @@ export function useEntryList({ journalId, bookmarkOnly = false }: Params) { const sorted = [...filtered].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); return { entries: sorted }; -} +}; diff --git a/src/hooks/entry/use-entry.ts b/src/hooks/entry/use-entry.ts index 5780322..bb9929b 100644 --- a/src/hooks/entry/use-entry.ts +++ b/src/hooks/entry/use-entry.ts @@ -3,7 +3,6 @@ import { useRef } from "react"; import * as Crypto from "expo-crypto"; import { z } from "zod"; -import type { FieldType } from "@/constants/journal"; import { storeEntry, updateEntryValues } from "@/db/queries/entries"; import { entrySelectSchema, @@ -12,58 +11,10 @@ import { type EntryValueObj, type FieldObj, } from "@/db/schemas"; +import { type FieldValue, getDefaultValue, serializeValue } from "@/utils/entry/field-value"; -export const fieldValueSchema = z.union([z.string(), z.number(), z.boolean(), z.date(), z.null()]); -export type FieldValue = z.infer; - -/** - * 各エントリー入力のデフォルト値 - * @param type フィールドタイプ - */ -const getDefaultValue = (type: FieldType): FieldValue => { - switch (type) { - case "text": - case "longText": - return ""; - case "check": - return false; - case "date": - case "time": - return new Date(); - default: - return null; - } -}; - -/** FieldValue を DB の text 型に変換 */ -const serializeValue = (value: FieldValue): string | null => { - if (value === null) return null; - if (value instanceof Date) return String(value.getTime()); - return String(value); -}; - -/** - * DB の text 型を FieldValue に変換 - * @param value DB の値 - * @param type フィールドタイプ - */ -export const deserializeValue = (value: string | null, type: FieldType): FieldValue => { - if (value === null) return getDefaultValue(type); - switch (type) { - case "number": - return Number(value); - case "check": - return value === "true"; - case "date": - case "time": - return new Date(Number(value)); - case "media": - case "location": - return null; - default: - return value; - } -}; +export type { FieldValue } from "@/utils/entry/field-value"; +export { deserializeValue } from "@/utils/entry/field-value"; /** * エントリーフォームの値を管理するフック diff --git a/src/hooks/journal/use-journal-field.ts b/src/hooks/journal/use-journal-field.ts index 27102d3..3ed74b5 100644 --- a/src/hooks/journal/use-journal-field.ts +++ b/src/hooks/journal/use-journal-field.ts @@ -3,44 +3,23 @@ import { useState } from "react"; import * as Crypto from "expo-crypto"; import { z } from "zod"; -import { - fieldTypeSchema, - type FieldType, - journalIconSchema, - JOURNAL_ICONS, -} from "@/constants/journal"; +import { type FieldType, JOURNAL_ICONS } from "@/constants/journal"; import { storeJournal, updateJournal as updateJournalQuery } from "@/db/queries/journals"; -import { fieldInsertSchema, type JournalObj } from "@/db/schemas"; -import { hexColorSchema } from "@/utils/journal/color"; - -/** - * ジャーナルメタ情報のスキーマ - */ -export const journalMetaSchema = z.object({ - name: z.string().trim().min(1).max(20), // 20文字まで - color: hexColorSchema, - icon: journalIconSchema, -}); -export type JournalMetaObj = z.infer; - -/** - * ジャーナル作成フォームのフィールド下書き(journalId・sortOrder なし) - */ -export const fieldDraftSchema = fieldInsertSchema - .omit({ journalId: true, sortOrder: true }) - .extend({ label: z.string().trim().min(1).max(30) }); // 30 文字まで -export type FieldDraftObj = z.infer; - -/** - * sortOrder 確定済み・journalId 未割当のフィールド(DB 保存直前) - */ -export const fieldWithSortSchema = fieldInsertSchema.omit({ journalId: true }); -export type FieldWithSortObj = z.infer; - -/** - * 全 FieldType の配列 - */ -export const FIELD_TYPES: FieldType[] = fieldTypeSchema.options; +import { type JournalObj } from "@/db/schemas"; +import { + type FieldDraftObj, + type FieldWithSortObj, + type JournalMetaObj, + fieldDraftSchema, + journalMetaSchema, +} from "@/utils/journal/journal-field"; + +export type { + FieldDraftObj, + FieldWithSortObj, + JournalMetaObj, +} from "@/utils/journal/journal-field"; +export { FIELD_TYPES } from "@/utils/journal/journal-field"; const defaultMeta: JournalMetaObj = { name: "", diff --git a/src/hooks/settings/use-auto-reflection.ts b/src/hooks/settings/use-auto-reflection.ts index 3570f1d..2807160 100644 --- a/src/hooks/settings/use-auto-reflection.ts +++ b/src/hooks/settings/use-auto-reflection.ts @@ -5,19 +5,9 @@ import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { getEntriesByDateQuery } from "@/db/queries/entries"; import { getReflectionByDateQuery, storeReflection } from "@/db/queries/reflections"; import { useAIReflectionSettings } from "@/hooks/settings/use-ai-reflection-settings"; -import { startOfDay } from "@/utils/date"; +import { isPastTime, startOfDay } from "@/utils/date"; import { getReflection } from "@/utils/days/reflection/get-reflection"; -/** 現在時刻が reflectionTime を過ぎているか */ -const isPastReflectionTime = (reflectionTime: Date): boolean => { - const now = new Date(); - return ( - now.getHours() > reflectionTime.getHours() || - (now.getHours() === reflectionTime.getHours() && - now.getMinutes() >= reflectionTime.getMinutes()) - ); -}; - /** * アプリ起動時に今日の AI Reflection を自動生成するフック * @@ -27,7 +17,7 @@ const isPastReflectionTime = (reflectionTime: Date): boolean => { * 3. 今日の reflection が DB に未保存 * 4. 今日のエントリーが1件以上ある */ -export function useAutoReflection() { +export const useAutoReflection = () => { const { aiReflectionEnabled, aiModel, reflectionTime } = useAIReflectionSettings(); const today = useMemo(() => startOfDay(), []); @@ -39,7 +29,7 @@ export function useAutoReflection() { if (!aiReflectionEnabled) return; if (reflection) return; if (!entries || entries.length === 0) return; - if (!isPastReflectionTime(reflectionTime)) return; + if (!isPastTime(reflectionTime)) return; if (generating.current) return; generating.current = true; @@ -54,4 +44,4 @@ export function useAutoReflection() { } })(); }, [entries, reflection, aiReflectionEnabled, reflectionTime, aiModel.gguf, today]); -} +}; diff --git a/src/utils/date.ts b/src/utils/date.ts index e051e17..32669b8 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -63,3 +63,15 @@ export const addDays = (date: Date, days: number) => { d.setDate(d.getDate() + days); return startOfDay(d); }; + +/** + * 現在時刻が指定時刻を過ぎているか(時・分のみ比較) + * @param time 比較対象の時刻 + */ +export const isPastTime = (time: Date): boolean => { + const now = new Date(); + return ( + now.getHours() > time.getHours() || + (now.getHours() === time.getHours() && now.getMinutes() >= time.getMinutes()) + ); +}; diff --git a/src/utils/entry/entry-form.ts b/src/utils/entry/entry-form.ts index 5ac85fd..61979ef 100644 --- a/src/utils/entry/entry-form.ts +++ b/src/utils/entry/entry-form.ts @@ -1,6 +1,6 @@ import { EntryDetailObj } from "@/db/queries/entries"; import { FieldObj } from "@/db/schemas"; -import { deserializeValue, FieldValue } from "@/hooks/entry/use-entry"; +import { deserializeValue, FieldValue } from "@/utils/entry/field-value"; /** * エントリー詳細からフォーム用のフィールド一覧と初期値を導出する diff --git a/src/utils/entry/field-value.ts b/src/utils/entry/field-value.ts new file mode 100644 index 0000000..4342148 --- /dev/null +++ b/src/utils/entry/field-value.ts @@ -0,0 +1,55 @@ +import { z } from "zod"; + +import type { FieldType } from "@/constants/journal"; + +export const fieldValueSchema = z.union([z.string(), z.number(), z.boolean(), z.date(), z.null()]); +export type FieldValue = z.infer; + +/** + * 各エントリー入力のデフォルト値 + * @param type フィールドタイプ + */ +export const getDefaultValue = (type: FieldType): FieldValue => { + switch (type) { + case "text": + case "longText": + return ""; + case "check": + return false; + case "date": + case "time": + return new Date(); + default: + return null; + } +}; + +/** FieldValue を DB の text 型に変換 */ +export const serializeValue = (value: FieldValue): string | null => { + if (value === null) return null; + if (value instanceof Date) return String(value.getTime()); + return String(value); +}; + +/** + * DB の text 型を FieldValue に変換 + * @param value DB の値 + * @param type フィールドタイプ + */ +export const deserializeValue = (value: string | null, type: FieldType): FieldValue => { + if (value === null) return getDefaultValue(type); + switch (type) { + case "number": + return Number(value); + case "check": + return value === "true"; + case "date": + case "time": + return new Date(Number(value)); + case "media": + case "location": + return null; + default: + return value; + } +}; diff --git a/src/utils/journal/journal-field.ts b/src/utils/journal/journal-field.ts new file mode 100644 index 0000000..c8693f9 --- /dev/null +++ b/src/utils/journal/journal-field.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; + +import { fieldTypeSchema, type FieldType, journalIconSchema } from "@/constants/journal"; +import { fieldInsertSchema } from "@/db/schemas"; + +import { hexColorSchema } from "./color"; + +/** + * ジャーナルメタ情報のスキーマ + */ +export const journalMetaSchema = z.object({ + name: z.string().trim().min(1).max(20), // 20文字まで + color: hexColorSchema, + icon: journalIconSchema, +}); +export type JournalMetaObj = z.infer; + +/** + * ジャーナル作成フォームのフィールド下書き(journalId・sortOrder なし) + */ +export const fieldDraftSchema = fieldInsertSchema + .omit({ journalId: true, sortOrder: true }) + .extend({ label: z.string().trim().min(1).max(30) }); // 30 文字まで +export type FieldDraftObj = z.infer; + +/** + * sortOrder 確定済み・journalId 未割当のフィールド(DB 保存直前) + */ +export const fieldWithSortSchema = fieldInsertSchema.omit({ journalId: true }); +export type FieldWithSortObj = z.infer; + +/** + * 全 FieldType の配列 + */ +export const FIELD_TYPES: FieldType[] = fieldTypeSchema.options; From 52667850cc29ae054c7f3583068b947a356da3c7 Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 16 Aug 2026 20:15:09 +0900 Subject: [PATCH 28/32] =?UTF-8?q?refactor:=20=E3=83=90=E3=83=AA=E3=83=87?= =?UTF-8?q?=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=BE=E3=82=8F=E3=82=8A?= =?UTF-8?q?=E3=81=AE=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/edit.tsx | 6 +++-- src/app/(journal)/entry/[id].tsx | 25 ++++++++----------- src/app/(journal)/entry/create.tsx | 7 +++--- src/hooks/use-theme.ts | 4 +-- .../use-validated-params.ts} | 4 +-- 5 files changed, 21 insertions(+), 25 deletions(-) rename src/{utils/params.ts => hooks/use-validated-params.ts} (73%) diff --git a/src/app/(journal)/edit.tsx b/src/app/(journal)/edit.tsx index 5fb5166..b7eed3f 100644 --- a/src/app/(journal)/edit.tsx +++ b/src/app/(journal)/edit.tsx @@ -10,8 +10,8 @@ import { JournalCreateView } from "@/components/journal/journal-create-view"; import { deleteJournal, getJournalDetailQuery, JournalDetail } from "@/db/queries/journals"; import { FieldObj } from "@/db/schemas"; import { FieldDraftObj, useJournalField } from "@/hooks/journal/use-journal-field"; +import { useValidatedParams } from "@/hooks/use-validated-params"; import { exportJournal } from "@/utils/days/export-journal"; -import { useValidatedParams } from "@/utils/params"; type FormProps = { journal: JournalDetail; @@ -155,7 +155,9 @@ function JournalEditForm({ journal }: FormProps) { * ジャーナル編集 */ export default function JournalEditScreen() { - const { journalId } = useValidatedParams(z.object({ journalId: z.string() })); + const schema = z.object({ journalId: z.string() }); + const { journalId } = useValidatedParams(schema); + const { data: journal } = useLiveQuery(getJournalDetailQuery(journalId), [journalId]); return <>{journal && }; diff --git a/src/app/(journal)/entry/[id].tsx b/src/app/(journal)/entry/[id].tsx index 6d7f89f..e3c34f8 100644 --- a/src/app/(journal)/entry/[id].tsx +++ b/src/app/(journal)/entry/[id].tsx @@ -7,7 +7,7 @@ import { z } from "zod"; import { EntryCreateView } from "@/components/entry/entry-create-view"; import { EntryDetailView } from "@/components/entry/entry-detail"; import { useEntryDetail } from "@/hooks/entry/use-entry-detail"; -import { useValidatedParams } from "@/utils/params"; +import { useValidatedParams } from "@/hooks/use-validated-params"; /** * エントリー詳細 @@ -15,20 +15,15 @@ import { useValidatedParams } from "@/utils/params"; export default function EntryDetailScreen() { const router = useRouter(); - const { - id: entryId, - journalName, - edit, - } = useValidatedParams( - z.object({ - id: z.string(), - journalName: z.string(), - edit: z - .string() - .optional() - .transform((v) => v === "true"), - }), - ); + const shema = z.object({ + id: z.string(), + journalName: z.string(), + edit: z + .string() + .optional() + .transform((v) => v === "true"), + }); + const { id: entryId, journalName, edit } = useValidatedParams(shema); const [editMode, setEditMode] = useState(edit); diff --git a/src/app/(journal)/entry/create.tsx b/src/app/(journal)/entry/create.tsx index fb35e1d..35e5640 100644 --- a/src/app/(journal)/entry/create.tsx +++ b/src/app/(journal)/entry/create.tsx @@ -7,7 +7,7 @@ import { z } from "zod"; import { EntryCreateView } from "@/components/entry/entry-create-view"; import { getFieldsQuery } from "@/db/queries/fields"; import { useEntry } from "@/hooks/entry/use-entry"; -import { useValidatedParams } from "@/utils/params"; +import { useValidatedParams } from "@/hooks/use-validated-params"; /** * エントリー作成 @@ -15,9 +15,8 @@ import { useValidatedParams } from "@/utils/params"; export default function EntryCreateScreen() { const router = useRouter(); - const { journalId, journalName } = useValidatedParams( - z.object({ journalId: z.string(), journalName: z.string() }), - ); + const schema = z.object({ journalId: z.string(), journalName: z.string() }); + const { journalId, journalName } = useValidatedParams(schema); const { data: fields } = useLiveQuery(getFieldsQuery(journalId), [journalId]); const { valuesRef, setValue, createEntry } = useEntry(fields); diff --git a/src/hooks/use-theme.ts b/src/hooks/use-theme.ts index 4b6210d..c82f200 100644 --- a/src/hooks/use-theme.ts +++ b/src/hooks/use-theme.ts @@ -6,9 +6,9 @@ import { Colors } from "@/constants/theme"; import { useColorScheme } from "@/hooks/use-color-scheme"; -export function useTheme() { +export const useTheme = () => { const scheme = useColorScheme(); const theme = scheme === "unspecified" ? "light" : scheme; return Colors[theme]; -} +}; diff --git a/src/utils/params.ts b/src/hooks/use-validated-params.ts similarity index 73% rename from src/utils/params.ts rename to src/hooks/use-validated-params.ts index d550fe7..56491e8 100644 --- a/src/utils/params.ts +++ b/src/hooks/use-validated-params.ts @@ -5,7 +5,7 @@ import { useLocalSearchParams } from "expo-router"; /** * useLocalSearchParams を Zod スキーマでバリデーションするフック */ -export function useValidatedParams(schema: T): z.infer { +export const useValidatedParams = (schema: T): z.infer => { const params = useLocalSearchParams(); return schema.parse(params); -} +}; From 6bae3c00eea5c057549b52838b6faddbaf2cb2bc Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 16 Aug 2026 20:23:23 +0900 Subject: [PATCH 29/32] chore: update claude.md --- CLAUDE.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 65e2897..0a9985f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`) + ### 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 From bde7afa5c2af1ed0798322d37e5665c159444fcf Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 16 Aug 2026 20:57:45 +0900 Subject: [PATCH 30/32] fix: review fix --- src/app/(journal)/entry/[id].tsx | 133 ++++++++------------ src/components/entry/entry-edit-view.tsx | 67 ++++++++++ src/components/settings/application.tsx | 9 +- src/components/settings/entry.tsx | 18 ++- src/constants/reflection.ts | 6 +- src/hooks/entry/use-entry-detail.ts | 40 ------ src/utils/days/reflection/get-reflection.ts | 11 +- 7 files changed, 146 insertions(+), 138 deletions(-) create mode 100644 src/components/entry/entry-edit-view.tsx delete mode 100644 src/hooks/entry/use-entry-detail.ts diff --git a/src/app/(journal)/entry/[id].tsx b/src/app/(journal)/entry/[id].tsx index e3c34f8..02a09a4 100644 --- a/src/app/(journal)/entry/[id].tsx +++ b/src/app/(journal)/entry/[id].tsx @@ -1,12 +1,13 @@ import { useState } from "react"; -import { Alert, PlatformColor } from "react-native"; +import { PlatformColor } from "react-native"; +import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { Stack, useRouter } from "expo-router"; import { z } from "zod"; -import { EntryCreateView } from "@/components/entry/entry-create-view"; import { EntryDetailView } from "@/components/entry/entry-detail"; -import { useEntryDetail } from "@/hooks/entry/use-entry-detail"; +import { EntryEditView } from "@/components/entry/entry-edit-view"; +import { bookmarkEntry, deleteEntry, getEntryDetailQuery } from "@/db/queries/entries"; import { useValidatedParams } from "@/hooks/use-validated-params"; /** @@ -26,100 +27,76 @@ export default function EntryDetailScreen() { const { id: entryId, journalName, edit } = useValidatedParams(shema); const [editMode, setEditMode] = useState(edit); - - const { entry, valuesRef, setValue, save, bookmark, remove } = useEntryDetail(entryId); - - const handleSave = async () => { - try { - await save(); - setEditMode(false); - } catch (error) { - if (error instanceof z.ZodError) { - Alert.alert("Validation Error", error.issues[0].message); - } - } - }; + const { data: entry } = useLiveQuery(getEntryDetailQuery(entryId), [entryId]); const handleDelete = async () => { - await remove(); + await deleteEntry(entryId); router.back(); }; + const handleBookmark = async () => { + if (entry) await bookmarkEntry(entry.id, !entry.bookmark); + }; + return ( <> - editMode - ? [ - { - type: "button", - label: "Cancel", - onPress: () => setEditMode(false), - }, - { - type: "button", - label: "Save", - icon: { type: "sfSymbol", name: "checkmark" }, - tintColor: PlatformColor("systemIndigo"), - variant: "prominent", - onPress: handleSave, - }, - ] - : [ - { - type: "menu", - label: "Options", - icon: { type: "sfSymbol", name: "ellipsis" }, - menu: { - items: [ - { - type: "action", - icon: { - type: "sfSymbol", - name: "square.and.arrow.up", - }, - label: "Export Entry", - onPress: () => console.log("Export Entry"), + unstable_headerRightItems: editMode + ? undefined + : () => [ + { + type: "menu", + label: "Options", + icon: { type: "sfSymbol", name: "ellipsis" }, + menu: { + items: [ + { + type: "action", + icon: { + type: "sfSymbol", + name: "square.and.arrow.up", }, - { - type: "action", - label: "Delete Entry", - icon: { type: "sfSymbol", name: "trash" }, - destructive: true, - onPress: handleDelete, - }, - ], - }, - }, - { - type: "button", - icon: { - type: "sfSymbol", - name: entry?.bookmark ? "bookmark.fill" : "bookmark", - }, - label: entry?.bookmark ? "Unbookmark" : "Bookmark", - tintColor: entry?.bookmark ? PlatformColor("systemIndigo") : undefined, - onPress: bookmark, + label: "Export Entry", + onPress: () => console.log("Export Entry"), + }, + { + type: "action", + label: "Delete Entry", + icon: { type: "sfSymbol", name: "trash" }, + destructive: true, + onPress: handleDelete, + }, + ], }, - { - type: "button", - label: "Edit", - onPress: () => setEditMode(true), + }, + { + type: "button", + icon: { + type: "sfSymbol", + name: entry?.bookmark ? "bookmark.fill" : "bookmark", }, - ], + label: entry?.bookmark ? "Unbookmark" : "Bookmark", + tintColor: entry?.bookmark ? PlatformColor("systemIndigo") : undefined, + onPress: handleBookmark, + }, + { + type: "button", + label: "Edit", + onPress: () => setEditMode(true), + }, + ], }} /> {entry && (editMode ? ( - setEditMode(false)} + onCancel={() => setEditMode(false)} /> ) : ( diff --git a/src/components/entry/entry-edit-view.tsx b/src/components/entry/entry-edit-view.tsx new file mode 100644 index 0000000..b1f392d --- /dev/null +++ b/src/components/entry/entry-edit-view.tsx @@ -0,0 +1,67 @@ +import { Alert, Keyboard, PlatformColor } from "react-native"; + +import { Stack } from "expo-router"; +import { z } from "zod"; + +import { EntryCreateView } from "@/components/entry/entry-create-view"; +import { type EntryDetailObj } from "@/db/queries/entries"; +import { useEntry } from "@/hooks/entry/use-entry"; +import { buildEntryFormData } from "@/utils/entry/entry-form"; + +/** + * 編集フォーム — editMode 切替時にマウント/アンマウントされ、useRef が毎回初期化される + */ +export function EntryEditView({ + entry, + onSave, + onCancel, +}: { + entry: EntryDetailObj; + onSave: () => void; + onCancel: () => void; +}) { + const { fields, initialValues } = buildEntryFormData(entry); + const { valuesRef, setValue, updateEntry } = useEntry(fields, initialValues); + + const handleSave = async () => { + try { + Keyboard.dismiss(); + await updateEntry(entry.id); + onSave(); + } catch (error) { + if (error instanceof z.ZodError) { + Alert.alert("Validation Error", error.issues[0].message); + } + } + }; + + return ( + <> + [ + { + type: "button", + label: "Cancel", + onPress: onCancel, + }, + { + type: "button", + label: "Save", + icon: { type: "sfSymbol", name: "checkmark" }, + tintColor: PlatformColor("systemIndigo"), + variant: "prominent", + onPress: handleSave, + }, + ], + }} + /> + + + ); +} diff --git a/src/components/settings/application.tsx b/src/components/settings/application.tsx index 3086992..1301369 100644 --- a/src/components/settings/application.tsx +++ b/src/components/settings/application.tsx @@ -21,7 +21,8 @@ export function Application() { setReflectionTime, } = useAIReflectionSettings(); const [downloading, setDownloading] = useState(false); - const [displayModelId, setDisplayModelId] = useState(aiModel.id); + const [pendingModelId, setPendingModelId] = useState(null); + const displayModelId = pendingModelId ?? aiModel.id; const handleModelSelect = (id: string) => { if (downloading) return; @@ -31,7 +32,7 @@ export function Application() { const newModel = AI_MODELS.find((m) => m.id === modelId); if (!newModel) return; - setDisplayModelId(modelId); + setPendingModelId(modelId); Alert.alert( "Download Model", @@ -40,7 +41,7 @@ export function Application() { { text: "Cancel", style: "cancel", - onPress: () => setDisplayModelId(aiModel.id), + onPress: () => setPendingModelId(null), }, { text: "OK", @@ -53,8 +54,8 @@ export function Application() { await removeModel(oldGguf).catch(() => {}); } catch (error) { console.warn("[model-switch]", error); - setDisplayModelId(aiModel.id); } finally { + setPendingModelId(null); setDownloading(false); } }, diff --git a/src/components/settings/entry.tsx b/src/components/settings/entry.tsx index 9332ffe..b22e426 100644 --- a/src/components/settings/entry.tsx +++ b/src/components/settings/entry.tsx @@ -1,21 +1,19 @@ -import { PlatformColor } from "react-native"; - import { Button, Section, Text } from "@expo/ui/swift-ui"; import { foregroundStyle } from "@expo/ui/swift-ui/modifiers"; +const primaryStyle = foregroundStyle({ type: "hierarchical", style: "primary" }); + export function EntrySettings() { return (
- - -
); diff --git a/src/constants/reflection.ts b/src/constants/reflection.ts index 76ff80d..2c080b0 100644 --- a/src/constants/reflection.ts +++ b/src/constants/reflection.ts @@ -61,8 +61,10 @@ export const reflectionSchema = z.object({ items: z .array( z.object({ - category: z.string().refine((v): v is ReflectionCategory => v in reflectionCategories), - content: z.string().min(1), + category: z + .string() + .refine((v): v is ReflectionCategory => Object.hasOwn(reflectionCategories, v)), + content: z.string().min(1).max(80), }), ) .length(2), diff --git a/src/hooks/entry/use-entry-detail.ts b/src/hooks/entry/use-entry-detail.ts deleted file mode 100644 index eb6ece9..0000000 --- a/src/hooks/entry/use-entry-detail.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Keyboard } from "react-native"; - -import { useLiveQuery } from "drizzle-orm/expo-sqlite"; - -import { bookmarkEntry, deleteEntry, getEntryDetailQuery } from "@/db/queries/entries"; -import { buildEntryFormData } from "@/utils/entry/entry-form"; - -import { useEntry } from "./use-entry"; - -/** - * エントリー詳細画面のデータ取得・フォーム状態・アクションをまとめたフック - * @param entryId エントリー id - */ -export const useEntryDetail = (entryId: string) => { - const { data: entry } = useLiveQuery(getEntryDetailQuery(entryId), [entryId]); - - const { fields, initialValues } = entry - ? buildEntryFormData(entry) - : { fields: [], initialValues: null }; - - const { valuesRef, setValue, updateEntry } = useEntry(fields, initialValues); - - /** 編集内容を保存する */ - const save = async () => { - Keyboard.dismiss(); - await updateEntry(entryId); - }; - - /** ブックマーク状態を切り替える */ - const bookmark = async () => { - if (entry) await bookmarkEntry(entry.id, !entry.bookmark); - }; - - /** エントリーを削除する */ - const remove = async () => { - await deleteEntry(entryId); - }; - - return { entry, valuesRef, setValue, save, bookmark, remove }; -}; diff --git a/src/utils/days/reflection/get-reflection.ts b/src/utils/days/reflection/get-reflection.ts index f6fb8f8..82e54b4 100644 --- a/src/utils/days/reflection/get-reflection.ts +++ b/src/utils/days/reflection/get-reflection.ts @@ -40,12 +40,13 @@ export const getReflection = async ( ): Promise => { const modelPath = await downloadModel(gguf); const model = llama.languageModel(modelPath); - await model.prepare(); - - const entriesText = entriesToText(entries); - const prompt = `Here are today's journal entries. Generate a reflection based on these records.\n\n${entriesText}`; try { + await model.prepare(); + + const entriesText = entriesToText(entries); + const prompt = `Here are today's journal entries. Generate a reflection based on these records.\n\n${entriesText}`; + const { text } = await generateText({ model, system: buildSystemPrompt(categoryList, "ja"), @@ -60,5 +61,7 @@ export const getReflection = async ( } catch (error) { console.warn("[reflection]", error); return null; + } finally { + await model.unload(); } }; From 6e241454f699776969f8232718ea5afcd441ab35 Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 16 Aug 2026 20:59:40 +0900 Subject: [PATCH 31/32] =?UTF-8?q?feat:=20=E4=B8=8D=E8=A6=81=E3=81=AA?= =?UTF-8?q?=E3=83=91=E3=83=83=E3=82=B1=E3=83=BC=E3=82=B8=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 - pnpm-lock.yaml | 15 --------------- 2 files changed, 16 deletions(-) diff --git a/package.json b/package.json index 7e05c7b..9a75a07 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ }, "dependencies": { "@expo/ui": "~55.0.17", - "@react-native-ai/apple": "^0.12.0", "@react-native-ai/llama": "^0.12.0", "@react-navigation/bottom-tabs": "^7.15.5", "@react-navigation/elements": "^2.9.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99b8ae2..1c2f335 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,9 +21,6 @@ importers: '@expo/ui': specifier: ~55.0.17 version: 55.0.17(expo@55.0.18)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - '@react-native-ai/apple': - specifier: ^0.12.0 - version: 0.12.0(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)) '@react-native-ai/llama': specifier: ^0.12.0 version: 0.12.0(llama.rn@0.12.0-rc.9(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) @@ -2039,11 +2036,6 @@ packages: '@types/react': optional: true - '@react-native-ai/apple@0.12.0': - resolution: {integrity: sha512-BC/kEDbCZprv1xcQCWsgapXm/WEsj5lieBDvU1vJXStrN+BG+hWM01zoqDGTD/j71zvt9DEfHcMvEqbrGY2uAA==} - peerDependencies: - react-native: '>=0.76.0' - '@react-native-ai/llama@0.12.0': resolution: {integrity: sha512-v/zhk9SEAk7P4/iELHMkAfIEEIn1TaMdIwfaFv41Yu3wm3sRbws7qRufTeq3nFBnSLj3MtVMZy4Vew7Fy5uyaA==} peerDependencies: @@ -7838,13 +7830,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - '@react-native-ai/apple@0.12.0(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))': - dependencies: - '@ai-sdk/provider': 3.0.14 - '@ai-sdk/provider-utils': 4.0.42(zod@4.4.3) - react-native: 0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) - zod: 4.4.3 - '@react-native-ai/llama@0.12.0(llama.rn@0.12.0-rc.9(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': dependencies: '@ai-sdk/provider': 3.0.14 From 4dd78fc9e19ef2d13a2ad0540db49468fb3af685 Mon Sep 17 00:00:00 2001 From: 273* Date: Sun, 16 Aug 2026 22:29:51 +0900 Subject: [PATCH 32/32] =?UTF-8?q?refactor:=20=E4=BF=9D=E5=AD=98=E5=87=A6?= =?UTF-8?q?=E7=90=86=E5=A4=B1=E6=95=97=E3=81=BE=E3=82=8F=E3=82=8A=E3=81=AE?= =?UTF-8?q?=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(journal)/create.tsx | 8 +++----- src/app/(journal)/edit.tsx | 7 +++---- src/app/(journal)/entry/create.tsx | 7 +++---- src/components/entry/entry-edit-view.tsx | 8 +++----- src/utils/handle-save-error.ts | 16 ++++++++++++++++ 5 files changed, 28 insertions(+), 18 deletions(-) create mode 100644 src/utils/handle-save-error.ts diff --git a/src/app/(journal)/create.tsx b/src/app/(journal)/create.tsx index 2f081b4..eede295 100644 --- a/src/app/(journal)/create.tsx +++ b/src/app/(journal)/create.tsx @@ -1,13 +1,13 @@ import { useState } from "react"; -import { Alert, Keyboard, PlatformColor } from "react-native"; +import { Keyboard, PlatformColor } from "react-native"; import * as Crypto from "expo-crypto"; import { Stack, useRouter } from "expo-router"; -import { z } from "zod"; import { JournalCreateView } from "@/components/journal/journal-create-view"; import { useJournalField } from "@/hooks/journal/use-journal-field"; import { importJournal } from "@/utils/days/import-journal"; +import { handleSaveError } from "@/utils/handle-save-error"; import { setCreatedJournalId } from "@/utils/journal/created-journal"; /** @@ -39,9 +39,7 @@ export default function JournalCreateScreen() { setCreatedJournalId(id); router.back(); } catch (error) { - if (error instanceof z.ZodError) { - Alert.alert("Validation Error", error.issues[0].message); - } + handleSaveError(error); } }; diff --git a/src/app/(journal)/edit.tsx b/src/app/(journal)/edit.tsx index b7eed3f..08d2e75 100644 --- a/src/app/(journal)/edit.tsx +++ b/src/app/(journal)/edit.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Alert as RNAlert, Keyboard, PlatformColor } from "react-native"; +import { Keyboard, PlatformColor } from "react-native"; import { Alert, Button, Host, Text } from "@expo/ui/swift-ui"; import { useLiveQuery } from "drizzle-orm/expo-sqlite"; @@ -12,6 +12,7 @@ import { FieldObj } from "@/db/schemas"; import { FieldDraftObj, useJournalField } from "@/hooks/journal/use-journal-field"; import { useValidatedParams } from "@/hooks/use-validated-params"; import { exportJournal } from "@/utils/days/export-journal"; +import { handleSaveError } from "@/utils/handle-save-error"; type FormProps = { journal: JournalDetail; @@ -54,9 +55,7 @@ function JournalEditForm({ journal }: FormProps) { await updateJournal(journal.id); router.back(); } catch (error) { - if (error instanceof z.ZodError) { - RNAlert.alert("Validation Error", error.issues[0].message); - } + handleSaveError(error); } }; diff --git a/src/app/(journal)/entry/create.tsx b/src/app/(journal)/entry/create.tsx index 35e5640..6f34b3e 100644 --- a/src/app/(journal)/entry/create.tsx +++ b/src/app/(journal)/entry/create.tsx @@ -1,4 +1,4 @@ -import { Alert, Keyboard, PlatformColor } from "react-native"; +import { Keyboard, PlatformColor } from "react-native"; import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { Stack, useRouter } from "expo-router"; @@ -8,6 +8,7 @@ import { EntryCreateView } from "@/components/entry/entry-create-view"; import { getFieldsQuery } from "@/db/queries/fields"; import { useEntry } from "@/hooks/entry/use-entry"; import { useValidatedParams } from "@/hooks/use-validated-params"; +import { handleSaveError } from "@/utils/handle-save-error"; /** * エントリー作成 @@ -28,9 +29,7 @@ export default function EntryCreateScreen() { const { id: newEntryId } = await createEntry(journalId); router.replace(`/(journal)/entry/${newEntryId}?journalName=${journalName}`); } catch (error) { - if (error instanceof z.ZodError) { - Alert.alert("Validation Error", error.issues[0].message); - } + handleSaveError(error); } }; diff --git a/src/components/entry/entry-edit-view.tsx b/src/components/entry/entry-edit-view.tsx index b1f392d..c609287 100644 --- a/src/components/entry/entry-edit-view.tsx +++ b/src/components/entry/entry-edit-view.tsx @@ -1,12 +1,12 @@ -import { Alert, Keyboard, PlatformColor } from "react-native"; +import { Keyboard, PlatformColor } from "react-native"; import { Stack } from "expo-router"; -import { z } from "zod"; import { EntryCreateView } from "@/components/entry/entry-create-view"; import { type EntryDetailObj } from "@/db/queries/entries"; import { useEntry } from "@/hooks/entry/use-entry"; import { buildEntryFormData } from "@/utils/entry/entry-form"; +import { handleSaveError } from "@/utils/handle-save-error"; /** * 編集フォーム — editMode 切替時にマウント/アンマウントされ、useRef が毎回初期化される @@ -29,9 +29,7 @@ export function EntryEditView({ await updateEntry(entry.id); onSave(); } catch (error) { - if (error instanceof z.ZodError) { - Alert.alert("Validation Error", error.issues[0].message); - } + handleSaveError(error); } }; diff --git a/src/utils/handle-save-error.ts b/src/utils/handle-save-error.ts new file mode 100644 index 0000000..68f344d --- /dev/null +++ b/src/utils/handle-save-error.ts @@ -0,0 +1,16 @@ +import { Alert } from "react-native"; + +import { z } from "zod"; + +/** + * 保存・更新処理の共通エラーハンドリング + * - ZodError → バリデーションエラーを表示 + * - その他 → 汎用エラーを表示 + */ +export const handleSaveError = (error: unknown) => { + if (error instanceof z.ZodError) { + Alert.alert("Validation Error", error.issues[0].message); + } else { + Alert.alert("Error", "An unexpected error occurred. Please try again."); + } +};