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: {
>