From fc7c223b89600076f3dafc521394b4a8855835e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 Nov 2024 10:30:21 +0000 Subject: [PATCH 01/10] chore(deps): bump astro from 4.16.11 to 4.16.13 Bumps [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) from 4.16.11 to 4.16.13. - [Release notes](https://github.com/withastro/astro/releases) - [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md) - [Commits](https://github.com/withastro/astro/commits/astro@4.16.13/packages/astro) --- updated-dependencies: - dependency-name: astro dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b2b5b23..79b5fdc 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", From 306b7d8d36d3fa92f2d57dfefc39f0d719abbb14 Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura Date: Sun, 17 Nov 2024 13:14:11 +0000 Subject: [PATCH 02/10] ai --- src/islands/ai-quiz/constants.ts | 2 +- src/islands/ai-quiz/quiz-manager.ts | 23 ++- src/islands/ai-quiz/utils/generate-quizzes.ts | 1 + .../note/components/notes/TextNote/AI.tsx | 136 ++++-------------- src/islands/shared/llm.ts | 53 +++++++ src/islands/shared/sse.ts | 105 ++++++++++++++ src/pages/app/settings.astro | 17 --- 7 files changed, 192 insertions(+), 145 deletions(-) create mode 100644 src/islands/shared/llm.ts create mode 100644 src/islands/shared/sse.ts diff --git a/src/islands/ai-quiz/constants.ts b/src/islands/ai-quiz/constants.ts index e08f917..ff01442 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/quiz-manager.ts b/src/islands/ai-quiz/quiz-manager.ts index 5250bbb..2524046 100644 --- a/src/islands/ai-quiz/quiz-manager.ts +++ b/src/islands/ai-quiz/quiz-manager.ts @@ -10,29 +10,22 @@ 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' - +import { generate } from '../shared/llm' 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/ai-quiz/utils/generate-quizzes.ts b/src/islands/ai-quiz/utils/generate-quizzes.ts index 7361802..b83a8af 100644 --- a/src/islands/ai-quiz/utils/generate-quizzes.ts +++ b/src/islands/ai-quiz/utils/generate-quizzes.ts @@ -13,6 +13,7 @@ export const quizzesGenerator = () => { return null } + const model = gemini.getGenerativeModel({ model: 'gemini-1.5-flash', generationConfig: { diff --git a/src/islands/note/components/notes/TextNote/AI.tsx b/src/islands/note/components/notes/TextNote/AI.tsx index 5290297..82c9709 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 { Spinner } from '../../utils/Spinner' +import { generate as generateLLM, generateStream } from '../../../../shared/llm' 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,11 @@ 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()), + generate: () =>generateStream({ + systemPrompt: 'ユーザーの指示に基づき、暗記の手助けになる赤シート用文章を生成しなさい。赤シートで隠すべき単語は、Markdownの太字機能で表現しなさい。隠す必要がない場所には太字は使わないでください。太字の場所はユーザーに見えません。また、単語の一覧を箇条書きにすることはしないでください。', + userPrompt: getPrompt() + }) }) setIsPreprocessing(false) } @@ -235,40 +205,21 @@ 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) } @@ -369,60 +320,21 @@ export const AIDialogCore = (props: { if (!streamer) { return } - let stream: GenerateContentStreamResult + let stream: Awaited> try { stream = await streamer.generate() - } catch (error) { - if (error instanceof GoogleGenerativeAIFetchError) { - switch (error.status) { - case 429: - setGenerateError({ type: 'RATE_LIMIT', isFetchError: true }) - break - case 500: - case 503: - setGenerateError({ - type: 'SERVICE_UNAVAILABLE', - isFetchError: true, - }) - break - default: - setGenerateError({ type: 'UNKNOWN', isFetchError: true }) - break - } - setIsGenerating(false) - return - } + } catch (e) { + console.error(e) setIsGenerating(false) + setGenerateError({ type: 'UNKNOWN', isFetchError: true }) return } - try { - for await (const chunk of stream.stream) { - setGenerated(`${getGenerated()}${chunk.text()}`) - outputRef.scrollTo({ - behavior: 'smooth', - top: 100 + outputRef.scrollHeight, - }) - } - } catch (error) { - if (error instanceof GoogleGenerativeAIResponseError) { - const response = ( - error as GoogleGenerativeAIResponseError - ).response - if (response?.candidates?.[0]?.finishReason === 'SAFETY') { - setGenerateError({ - type: 'SAFETY', - isFetchError: false, - }) - return - } - } - setGenerateError({ - type: 'UNKNOWN', - isFetchError: false, - }) - } finally { - setIsGenerating(false) + let text = '' + for await (const chunk of stream) { + text += chunk + setGenerated(text) } + setIsGenerating(false) } createEffect(() => { getStream() diff --git a/src/islands/shared/llm.ts b/src/islands/shared/llm.ts new file mode 100644 index 0000000..47b9c6b --- /dev/null +++ b/src/islands/shared/llm.ts @@ -0,0 +1,53 @@ +import { StepMap } from "@tiptap/pm/transform" +import { SSETransformStream } from "./sse" + +export interface GenerateInit { + userPrompt: string + systemPrompt?: string + image?: { + mimeType: string + data: string + } + jsonMode?: boolean +} +export const generate = async (init: GenerateInit) => { + const res = await fetch('https://mobile-app-contest-nanoha.deno.dev/generate', { + method: 'POST', + body: JSON.stringify({ ...init, stream: false }), + headers: { + 'Content-Type': 'application/json' + } + }) + if (!res.ok) { + throw new Error(`error: ${await res.text()}`) + } + return await res.text() +} +export const generateStream = async (init: GenerateInit) => { + const res = await fetch('https://mobile-app-contest-nanoha.deno.dev/generate', { + method: 'POST', + body: JSON.stringify({ ...init, stream: true }), + headers: { + 'Content-Type': 'application/json' + } + }) + if (!res.ok) { + throw new Error('Error') + } + if (!res.body) { + throw new Error('Body is null') + } + const stream = res.body.pipeThrough(new SSETransformStream()) + const reader = await stream.getReader() + return async function * () { + while (true) { + const { value, done } = await reader.read() + if (value) { + yield value.data ?? '' + } + if (done) { + return + } + } + }() +} \ No newline at end of file diff --git a/src/islands/shared/sse.ts b/src/islands/shared/sse.ts new file mode 100644 index 0000000..b99842c --- /dev/null +++ b/src/islands/shared/sse.ts @@ -0,0 +1,105 @@ + +interface SSEMessage { + event: string | null; + data: string; + id: string | null; + retry: number | null; +}; + +class SSETransformerContext implements Transformer { + private buffer = ""; + + transform(chunk: Uint8Array, controller: TransformStreamDefaultController) { + this.buffer += new TextDecoder().decode(chunk); + + const lines = this.buffer.split(/\r?\n/); + this.buffer = lines.pop() || ""; + + let event: string | null = null; + let data = ""; + let id: string | null = null; + let retry: number | null = null; + + for (const line of lines) { + if (line === "") { + if (data) { + controller.enqueue({ event, data: data.trim(), id, retry }); + } + event = null; + data = ""; + id = null; + retry = null; + continue; + } + + if (line.startsWith(":")) { + continue; + } + + const [field, ...rest] = line.split(":"); + const value = rest.join(":").trimStart(); + + switch (field) { + case "event": + event = value || null; + break; + case "data": + data += `${value}\n`; + break; + case "id": + id = value || null; + break; + case "retry": + retry = value ? Number.parseInt(value, 10) : null; + break; + } + } + } + + flush(controller: TransformStreamDefaultController) { + if (this.buffer) { + const lines = this.buffer.split(/\r?\n/); + let event: string | null = null; + let data = ""; + let id: string | null = null; + let retry: number | null = null; + + for (const line of lines) { + if (line === "") { + if (data) { + controller.enqueue({ event, data: data.trim(), id, retry }); + } + event = null; + data = ""; + id = null; + retry = null; + continue; + } + + const [field, ...rest] = line.split(":"); + const value = rest.join(":").trimStart(); + + switch (field) { + case "event": + event = value || null; + break; + case "data": + data += `${value}\n` + break; + case "id": + id = value || null; + break; + case "retry": + retry = value ? Number.parseInt(value, 10) : null; + break; + } + } + } + } +} + +export class SSETransformStream extends TransformStream { + constructor() { + super(new SSETransformerContext()) + } +} \ No newline at end of file diff --git a/src/pages/app/settings.astro b/src/pages/app/settings.astro index 96681ea..f17bc5c 100644 --- a/src/pages/app/settings.astro +++ b/src/pages/app/settings.astro @@ -7,22 +7,5 @@ import App from '../../layouts/App.astro'
Settings
-
-
-

AI

-
-
Gemini API Token
-
AI機能を使うのに必須です
- -
-
-
-

Info

-
-
Nanoha v{packageJson.version} Canary
-
Branch: { gitBranch }
-
Git hash: {commitHash}
-
-
From 49ca35cdf3d270369ee9db8c9addcae6cfd534f4 Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura <79000684+nakasyou@users.noreply.github.com> Date: Sun, 17 Nov 2024 22:14:49 +0900 Subject: [PATCH 03/10] Delete README.md --- README.md | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index 59e668c..0000000 --- a/README.md +++ /dev/null @@ -1,18 +0,0 @@ -
- -# Nanoha - -「じぶん」で作る、AI搭載学習用ノートブック -
- -## 開発 -### Develop with Bun - -開発モード: -```shell -bun dev -``` -ビルド: -```shell -bun run build -``` From 3a6cf5edf2e6815ccb3de9172429980e70943edd Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura <79000684+nakasyou@users.noreply.github.com> Date: Sun, 17 Nov 2024 22:29:49 +0900 Subject: [PATCH 04/10] Create README.txt --- README.txt | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 README.txt diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..15eba0f --- /dev/null +++ b/README.txt @@ -0,0 +1,45 @@ +# Nanoha | AI 搭載暗記用デジタルノートブック + +## OSバージョン + +OS バージョンは、Android 13、または iPadOS 16 で動作確認しています。 + +## ビルド環境 + +ビルドには、Bun という JavaScript ランタイムが必要です。右の URL からインストールしてください: https://bun.sh +Bun のインストールに失敗した、または Bun でのビルドが上手くいかない場合は、Node.js を使用できます。Node.js 22 および npm をセッティングアップしてください。 + +まず、プロジェクトルートで依存関係をインストールしてください: +``` +bun i # Bun を使用する場合 +npm i # Node.js を使用する場合 +``` + +ビルドは、以下のコマンドでできます: +``` +bun run build # Bun を使用する場合 +npm run build # Node.js を使用する場合 +``` + +## アプリに利用しているライブラリの一覧 + +依存関係の依存関係を含まないとすると、`package.json`に書いてあります。 + +## 利用しているDBの種類(利用されている方のみ) + +Web 技術上で実行可能な IndexedDB を使用しています。 +https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API + +## アプリの起動手順 + +ビルドしたら、サーバーを起動する必要があります: +``` +bun run start # Bun を使用する場合 +npm run start # Node.js を使用する場合 +``` + +そうすると、ポート `8080` でアプリが開始されます。 + +このアプリは PWA という技術を用いているため、スマホで動作させるにはサーバーに https 経由でアクセスする必要があります。そのためには、ローカル証明書を発行するか、リモートトンネルを作るか、Web ブラウザの設定を変える必要があります。 + +ローカル証明書の発行の仕方に詳しくないので、リモートトンネルを作る手順と、Webブラウン From c32cb1b66e70decf7a31764750d801406b5f9850 Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura <79000684+nakasyou@users.noreply.github.com> Date: Mon, 18 Nov 2024 08:54:44 +0900 Subject: [PATCH 05/10] Update README.txt --- README.txt | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/README.txt b/README.txt index 15eba0f..c5300d7 100644 --- a/README.txt +++ b/README.txt @@ -32,6 +32,8 @@ https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API ## アプリの起動手順 +### サーバーの起動 + ビルドしたら、サーバーを起動する必要があります: ``` bun run start # Bun を使用する場合 @@ -40,6 +42,31 @@ npm run start # Node.js を使用する場合 そうすると、ポート `8080` でアプリが開始されます。 +### SSL について + このアプリは PWA という技術を用いているため、スマホで動作させるにはサーバーに https 経由でアクセスする必要があります。そのためには、ローカル証明書を発行するか、リモートトンネルを作るか、Web ブラウザの設定を変える必要があります。 +ローカル証明書の発行の仕方に詳しくないので、リモートトンネルを作る手順と、Webブラウザの設定を変える方法を紹介します。 + +リモートトンネルを作る手順では、Cloudflare Tunnel を使う方法がおすすめです。 +``` +bunx untun tunnel http://localhost:8080 # Bun +npm untun tunnel http://localhost:8080 # Node.js +``` +でトンネルが起動できるので、その URL にモバイル端末からアクセスしてください。 + +Web ブラウザの設定を変える場合は、Android OS の Chrome である必要があります。 +まず、サーバーと同じネットワークに接続します。 +そして、 +chrome://flags/#unsafely-treat-insecure-origin-as-secure +を開き、そこにサーバーのプライベート IP アドレスを入力します。 +そうすると、完了です。 + +### アプリのインストール + +インストールには、「SSL について」で説明したトンネルのURLまたは、プライベートIPアドレスを入力してください。 +そして、Chromeの場合右上の3個の点から「アプリのインストール」を選択してください。Safariの場合、左下の共有アイコンから「ホーム画面に追加」を選択してください。 + +### アプリの起動 + +ホーム画面に追加されたアイコンをタップすると、起動されます。 -ローカル証明書の発行の仕方に詳しくないので、リモートトンネルを作る手順と、Webブラウン From 738be86a14024735a805e25121d41e91f168d985 Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura <79000684+nakasyou@users.noreply.github.com> Date: Tue, 19 Nov 2024 22:37:40 +0900 Subject: [PATCH 06/10] Delete README.txt --- README.txt | 72 ------------------------------------------------------ 1 file changed, 72 deletions(-) delete mode 100644 README.txt diff --git a/README.txt b/README.txt deleted file mode 100644 index c5300d7..0000000 --- a/README.txt +++ /dev/null @@ -1,72 +0,0 @@ -# Nanoha | AI 搭載暗記用デジタルノートブック - -## OSバージョン - -OS バージョンは、Android 13、または iPadOS 16 で動作確認しています。 - -## ビルド環境 - -ビルドには、Bun という JavaScript ランタイムが必要です。右の URL からインストールしてください: https://bun.sh -Bun のインストールに失敗した、または Bun でのビルドが上手くいかない場合は、Node.js を使用できます。Node.js 22 および npm をセッティングアップしてください。 - -まず、プロジェクトルートで依存関係をインストールしてください: -``` -bun i # Bun を使用する場合 -npm i # Node.js を使用する場合 -``` - -ビルドは、以下のコマンドでできます: -``` -bun run build # Bun を使用する場合 -npm run build # Node.js を使用する場合 -``` - -## アプリに利用しているライブラリの一覧 - -依存関係の依存関係を含まないとすると、`package.json`に書いてあります。 - -## 利用しているDBの種類(利用されている方のみ) - -Web 技術上で実行可能な IndexedDB を使用しています。 -https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API - -## アプリの起動手順 - -### サーバーの起動 - -ビルドしたら、サーバーを起動する必要があります: -``` -bun run start # Bun を使用する場合 -npm run start # Node.js を使用する場合 -``` - -そうすると、ポート `8080` でアプリが開始されます。 - -### SSL について - -このアプリは PWA という技術を用いているため、スマホで動作させるにはサーバーに https 経由でアクセスする必要があります。そのためには、ローカル証明書を発行するか、リモートトンネルを作るか、Web ブラウザの設定を変える必要があります。 -ローカル証明書の発行の仕方に詳しくないので、リモートトンネルを作る手順と、Webブラウザの設定を変える方法を紹介します。 - -リモートトンネルを作る手順では、Cloudflare Tunnel を使う方法がおすすめです。 -``` -bunx untun tunnel http://localhost:8080 # Bun -npm untun tunnel http://localhost:8080 # Node.js -``` -でトンネルが起動できるので、その URL にモバイル端末からアクセスしてください。 - -Web ブラウザの設定を変える場合は、Android OS の Chrome である必要があります。 -まず、サーバーと同じネットワークに接続します。 -そして、 -chrome://flags/#unsafely-treat-insecure-origin-as-secure -を開き、そこにサーバーのプライベート IP アドレスを入力します。 -そうすると、完了です。 - -### アプリのインストール - -インストールには、「SSL について」で説明したトンネルのURLまたは、プライベートIPアドレスを入力してください。 -そして、Chromeの場合右上の3個の点から「アプリのインストール」を選択してください。Safariの場合、左下の共有アイコンから「ホーム画面に追加」を選択してください。 - -### アプリの起動 - -ホーム画面に追加されたアイコンをタップすると、起動されます。 - From 2f7d3c6fe628f0009d8c2ce493cef3d756c5f8ba Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura <79000684+nakasyou@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:02:05 +0900 Subject: [PATCH 07/10] Update Base.astro --- src/layouts/Base.astro | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 29d97d1..86cefcf 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -50,10 +50,7 @@ nomalizedUrl.protocol = 'https://' } - { - Astro.props.noIndex && - } - + { // ViewTransitions Astro.props.useViewTransitions && From a2d96d9a37804d80203eefc62d0d851c78492d75 Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura Date: Sat, 30 Nov 2024 06:23:19 +0000 Subject: [PATCH 08/10] pwd --- src/integrations/cloudflare/server/server.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/integrations/cloudflare/server/server.ts b/src/integrations/cloudflare/server/server.ts index 8ad27ba..1c6e8a8 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 From cda0a476ae2a8c0c4e00c3177b2e0bbbeb21d98e Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura Date: Sat, 30 Nov 2024 06:24:30 +0000 Subject: [PATCH 09/10] format code --- src/integrations/cloudflare/server/server.ts | 2 +- src/islands/ai-quiz/QuizScreen.tsx | 12 +- src/islands/ai-quiz/index.tsx | 10 +- src/islands/ai-quiz/quiz-manager.ts | 12 +- src/islands/ai-quiz/utils/generate-quizzes.ts | 1 - .../note/components/notes/TextNote/AI.tsx | 32 ++-- src/islands/shared/llm.ts | 44 +++-- src/islands/shared/sse.ts | 171 +++++++++--------- 8 files changed, 150 insertions(+), 134 deletions(-) diff --git a/src/integrations/cloudflare/server/server.ts b/src/integrations/cloudflare/server/server.ts index 1c6e8a8..4b5419a 100644 --- a/src/integrations/cloudflare/server/server.ts +++ b/src/integrations/cloudflare/server/server.ts @@ -8,7 +8,7 @@ export function createExports(manifest: import('astro').SSRManifest) { const honoApp = new Hono() - honoApp.use(basicAuth({verifyUser: (username, password, c) => true ,})) + honoApp.use(basicAuth({ verifyUser: (username, password, c) => true })) honoApp.all('/*', async (c) => { const res = await app.render(c.req.raw) diff --git a/src/islands/ai-quiz/QuizScreen.tsx b/src/islands/ai-quiz/QuizScreen.tsx index 7fd8585..1124af0 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/index.tsx b/src/islands/ai-quiz/index.tsx index 4e9ebae..67f31dd 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 2524046..f048e5b 100644 --- a/src/islands/ai-quiz/quiz-manager.ts +++ b/src/islands/ai-quiz/quiz-manager.ts @@ -1,16 +1,16 @@ 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' -import { generate } from '../shared/llm' const generateQuizzesFromAI = async (text: string): Promise => { const gemini = getGoogleGenerativeAI() if (!gemini) { @@ -20,7 +20,7 @@ const generateQuizzesFromAI = async (text: string): Promise => { const response = await generate({ systemPrompt: PROMPT_TO_GENERATE_SELECT_QUIZ, userPrompt: text, - jsonMode: true + jsonMode: true, }) let json: unknown diff --git a/src/islands/ai-quiz/utils/generate-quizzes.ts b/src/islands/ai-quiz/utils/generate-quizzes.ts index b83a8af..7361802 100644 --- a/src/islands/ai-quiz/utils/generate-quizzes.ts +++ b/src/islands/ai-quiz/utils/generate-quizzes.ts @@ -13,7 +13,6 @@ export const quizzesGenerator = () => { return null } - const model = gemini.getGenerativeModel({ model: 'gemini-1.5-flash', generationConfig: { diff --git a/src/islands/note/components/notes/TextNote/AI.tsx b/src/islands/note/components/notes/TextNote/AI.tsx index 82c9709..3b8968a 100644 --- a/src/islands/note/components/notes/TextNote/AI.tsx +++ b/src/islands/note/components/notes/TextNote/AI.tsx @@ -12,8 +12,8 @@ import { onMount, untrack, } from 'solid-js' -import { Spinner } from '../../utils/Spinner' import { generate as generateLLM, generateStream } from '../../../../shared/llm' +import { Spinner } from '../../utils/Spinner' const markdownParser = markdownIt() @@ -145,10 +145,12 @@ export const FromText = (props: { const generate = async () => { setIsPreprocessing(true) props.setStream({ - generate: () =>generateStream({ - systemPrompt: 'ユーザーの指示に基づき、暗記の手助けになる赤シート用文章を生成しなさい。赤シートで隠すべき単語は、Markdownの太字機能で表現しなさい。隠す必要がない場所には太字は使わないでください。太字の場所はユーザーに見えません。また、単語の一覧を箇条書きにすることはしないでください。', - userPrompt: getPrompt() - }) + generate: () => + generateStream({ + systemPrompt: + 'ユーザーの指示に基づき、暗記の手助けになる赤シート用文章を生成しなさい。赤シートで隠すべき単語は、Markdownの太字機能で表現しなさい。隠す必要がない場所には太字は使わないでください。太字の場所はユーザーに見えません。また、単語の一覧を箇条書きにすることはしないでください。', + userPrompt: getPrompt(), + }), }) setIsPreprocessing(false) } @@ -213,12 +215,13 @@ export const FromImage = (props: { const generate = async () => await generateStream({ - systemPrompt: '画像を抽出し、そっくりそのまま書き出しなさい。省略せずに画像の文字全てを書き出すこと。画像に書いていないことは書かないこと。また、ユーザーからの指示があれば、条件を満たす場所をMarkdownの太字で表現しなさい。', - image: {mimeType: file.type,data: b64,}, - userPrompt: '' + systemPrompt: + '画像を抽出し、そっくりそのまま書き出しなさい。省略せずに画像の文字全てを書き出すこと。画像に書いていないことは書かないこと。また、ユーザーからの指示があれば、条件を満たす場所をMarkdownの太字で表現しなさい。', + image: { mimeType: file.type, data: b64 }, + userPrompt: '', }) - props.setStream({ - generate + props.setStream({ + generate, }) setIsPreprocessing(false) } @@ -235,9 +238,12 @@ export const FromImage = (props: { >
-
+