diff --git a/README.md b/README.md deleted file mode 100644 index 59e668ce..00000000 --- a/README.md +++ /dev/null @@ -1,18 +0,0 @@ -
- -# Nanoha - -「じぶん」で作る、AI搭載学習用ノートブック -
- -## 開発 -### Develop with Bun - -開発モード: -```shell -bun dev -``` -ビルド: -```shell -bun run build -``` diff --git a/package.json b/package.json index b2b5b232..7b02ca66 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@tiptap/pm": "^2.9.1", "@tiptap/starter-kit": "^2.9.1", "array-move": "^4.0.0", - "astro": "4.16.11", + "astro": "4.16.13", "classnames": "^2.5.1", "dedent": "^1.5.3", "dexie": "^4.0.9", @@ -43,7 +43,7 @@ "vite-imagetools": "^7.0.4" }, "devDependencies": { - "@astrojs/solid-js": "^4.4.2", + "@astrojs/solid-js": "^5.0.3", "@biomejs/biome": "^1.9.4", "@types/dompurify": "^3.0.5", "@types/fs-extra": "^11.0.4", diff --git a/src/integrations/cloudflare/server/server.ts b/src/integrations/cloudflare/server/server.ts index 8ad27ba9..4b5419a4 100644 --- a/src/integrations/cloudflare/server/server.ts +++ b/src/integrations/cloudflare/server/server.ts @@ -1,12 +1,15 @@ import { App } from 'astro/app' // @ts-check import { Hono } from 'hono' +import { basicAuth } from 'hono/basic-auth' export function createExports(manifest: import('astro').SSRManifest) { const app = new App(manifest) const honoApp = new Hono() + honoApp.use(basicAuth({ verifyUser: (username, password, c) => true })) + honoApp.all('/*', async (c) => { const res = await app.render(c.req.raw) return res diff --git a/src/islands/ai-quiz/QuizScreen.tsx b/src/islands/ai-quiz/QuizScreen.tsx index 7fd8585a..1124af06 100644 --- a/src/islands/ai-quiz/QuizScreen.tsx +++ b/src/islands/ai-quiz/QuizScreen.tsx @@ -1,18 +1,18 @@ import { + For, + Show, createEffect, createMemo, createSignal, - For, onMount, - Show, } from 'solid-js' -import { QuizManager, type GeneratedQuiz } from './quiz-manager' -import type { MargedNoteData } from '../note/components/notes-utils' -import { QuizDB, type Quizzes } from './storage' -import type { QuizContent } from './constants' import { shuffle } from '../../utils/arr' import { icon } from '../../utils/icons' +import type { MargedNoteData } from '../note/components/notes-utils' import type { TextNoteData } from '../note/components/notes/TextNote/types' +import type { QuizContent } from './constants' +import { type GeneratedQuiz, QuizManager } from './quiz-manager' +import { QuizDB, type Quizzes } from './storage' import './QuizScreen.css' export const finish = () => { diff --git a/src/islands/ai-quiz/constants.ts b/src/islands/ai-quiz/constants.ts index e08f9173..ff01442b 100644 --- a/src/islands/ai-quiz/constants.ts +++ b/src/islands/ai-quiz/constants.ts @@ -11,7 +11,7 @@ export const PROMPT_TO_GENERATE_SELECT_QUIZ = ` ユーザーからの文章に書かれていない問題は絶対に生成しないでください。 -以下のJSONスキーマに従いなさい。 +以下のJSONスキーマに従ったJSONを出力しなさい。また、出力はトップレベルで配列です。 ${JSON.stringify(selectQuestion)} `.trim() diff --git a/src/islands/ai-quiz/index.tsx b/src/islands/ai-quiz/index.tsx index 4e9ebae8..67f31dd7 100644 --- a/src/islands/ai-quiz/index.tsx +++ b/src/islands/ai-quiz/index.tsx @@ -1,10 +1,10 @@ -import { createSignal, onMount, Show } from 'solid-js' -import type { NoteLoadType } from '../note/note-load-types' -import { loadNoteFromType } from '../shared/storage' -import { load } from '../note/utils/file-format' +import { Show, createSignal, onMount } from 'solid-js' import type { MargedNoteData } from '../note/components/notes-utils' import { Spinner } from '../note/components/utils/Spinner' -import { finish, QuizScreen } from './QuizScreen' +import type { NoteLoadType } from '../note/note-load-types' +import { load } from '../note/utils/file-format' +import { loadNoteFromType } from '../shared/storage' +import { QuizScreen, finish } from './QuizScreen' export const Navbar = () => {} diff --git a/src/islands/ai-quiz/quiz-manager.ts b/src/islands/ai-quiz/quiz-manager.ts index 5250bbbc..f048e5be 100644 --- a/src/islands/ai-quiz/quiz-manager.ts +++ b/src/islands/ai-quiz/quiz-manager.ts @@ -1,38 +1,31 @@ import { parse, safeParse } from 'valibot' +import { shuffle } from '../../utils/arr' +import type { MargedNoteData } from '../note/components/notes-utils' +import type { TextNoteData } from '../note/components/notes/TextNote/types' import { getGoogleGenerativeAI } from '../shared/gemini' +import { sha256 } from '../shared/hash' +import { generate } from '../shared/llm' import { CONTENT_SCHEMA, PROMPT_TO_GENERATE_SELECT_QUIZ, type QuizContent, } from './constants' -import type { MargedNoteData } from '../note/components/notes-utils' -import type { TextNoteData } from '../note/components/notes/TextNote/types' import type { QuizDB, Quizzes } from './storage' -import { shuffle } from '../../utils/arr' -import { sha256 } from '../shared/hash' - const generateQuizzesFromAI = async (text: string): Promise => { const gemini = getGoogleGenerativeAI() if (!gemini) { throw new Error('Gemini is null.') } - const response = await gemini - .getGenerativeModel({ - model: 'gemini-1.5-flash', - generationConfig: { - responseMimeType: 'application/json', - }, - systemInstruction: { - role: 'system', - parts: [{ text: PROMPT_TO_GENERATE_SELECT_QUIZ }], - }, - }) - .startChat() - .sendMessage(text) + + const response = await generate({ + systemPrompt: PROMPT_TO_GENERATE_SELECT_QUIZ, + userPrompt: text, + jsonMode: true, + }) let json: unknown try { - json = JSON.parse(response.response.text()) + json = JSON.parse(response) } catch { return [] } diff --git a/src/islands/note/components/notes/TextNote/AI.tsx b/src/islands/note/components/notes/TextNote/AI.tsx index 5290297d..3b8968a4 100644 --- a/src/islands/note/components/notes/TextNote/AI.tsx +++ b/src/islands/note/components/notes/TextNote/AI.tsx @@ -1,10 +1,3 @@ -import { - type GenerateContentResponse, - type GenerateContentStreamResult, - type GoogleGenerativeAI, - GoogleGenerativeAIFetchError, - GoogleGenerativeAIResponseError, -} from '@google/generative-ai' import markdownIt from 'markdown-it' import { Match, @@ -19,14 +12,14 @@ import { onMount, untrack, } from 'solid-js' -import { getGoogleGenerativeAI } from '../../../../shared/gemini' +import { generate as generateLLM, generateStream } from '../../../../shared/llm' import { Spinner } from '../../utils/Spinner' const markdownParser = markdownIt() type Mode = 'text' | 'image' type Generate = { - generate: () => Promise + generate: () => ReturnType } const renderMarkdown = (markdown: string) => @@ -82,16 +75,6 @@ const FROM_TEXT_PLACEHOLDER_CONTENTS: string[] = [ '英語の曜日についての暗記シート', ] -const getGemini = (): GoogleGenerativeAI => { - const ai = getGoogleGenerativeAI() - if (!ai) { - if (confirm('AI 機能が設定されていません。\n設定を開きますか?')) { - location.href = '/app/settings#ai' - } - throw 0 - } - return ai -} export const FromText = (props: { setStream(stream: Generate): void initPrompt?: string @@ -161,24 +144,13 @@ export const FromText = (props: { const generate = async () => { setIsPreprocessing(true) - const gemini = getGemini() - const model = gemini.getGenerativeModel({ - model: 'gemini-1.5-flash', - }) props.setStream({ generate: () => - model - .startChat({ - systemInstruction: { - role: 'model', - parts: [ - { - text: 'ユーザーの指示に基づき、暗記の手助けになる赤シート用文章を生成しなさい。赤シートで隠すべき単語は、Markdownの太字機能で表現しなさい。隠す必要がない場所には太字は使わないでください。太字の場所はユーザーに見えません。また、単語の一覧を箇条書きにすることはしないでください。', - }, - ], - }, - }) - .sendMessageStream(getPrompt()), + generateStream({ + systemPrompt: + 'ユーザーの指示に基づき、暗記の手助けになる赤シート用文章を生成しなさい。赤シートで隠すべき単語は、Markdownの太字機能で表現しなさい。隠す必要がない場所には太字は使わないでください。太字の場所はユーザーに見えません。また、単語の一覧を箇条書きにすることはしないでください。', + userPrompt: getPrompt(), + }), }) setIsPreprocessing(false) } @@ -235,40 +207,22 @@ export const FromImage = (props: { return } - const ai = getGemini() - const model = ai.getGenerativeModel({ - model: 'gemini-1.5-pro', - }) const b64 = await new Promise((resolve) => { const reader = new FileReader() reader.onloadend = () => resolve((reader.result as string).split(',')[1]!) reader.readAsDataURL(file) }) - const generate = () => - model - .startChat({ - systemInstruction: { - role: 'model', - parts: [ - { - text: '画像を抽出し、そっくりそのまま書き出しなさい。省略せずに画像の文字全てを書き出すこと。画像に書いていないことは書かないこと。また、ユーザーからの指示があれば、条件を満たす場所をMarkdownの太字で表現しなさい。', - }, - ], - }, - }) - .sendMessageStream([ - { - text: '', - }, - { - inlineData: { - mimeType: file.type, - data: b64, - }, - }, - ]) - props.setStream({ generate }) + const generate = async () => + await generateStream({ + systemPrompt: + '画像を抽出し、そっくりそのまま書き出しなさい。省略せずに画像の文字全てを書き出すこと。画像に書いていないことは書かないこと。また、ユーザーからの指示があれば、条件を満たす場所をMarkdownの太字で表現しなさい。', + image: { mimeType: file.type, data: b64 }, + userPrompt: '', + }) + props.setStream({ + generate, + }) setIsPreprocessing(false) } @@ -284,9 +238,12 @@ export const FromImage = (props: { >
-
+