Skip to content

Commit

Permalink
Merge branch 'aitag' into preview
Browse files Browse the repository at this point in the history
  • Loading branch information
banzhe committed Dec 3, 2024
2 parents 6cb9b90 + b46ae84 commit 0dc1d04
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 18 deletions.
7 changes: 6 additions & 1 deletion packages/plugin/popup/components/TagInputWithCache.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import type { AutoCompleteTagInputRef } from '@web-archive/shared/components/aut
import AutoCompleteTagInput from '@web-archive/shared/components/auto-complete-tag-input'
import { Button } from '@web-archive/shared/components/button'
import type { GenerateTagProps } from '@web-archive/shared/utils'
import { generateTagByOpenAI } from '@web-archive/shared/utils'
import { generateTagByOpenAI, isNil } from '@web-archive/shared/utils'
import { useRequest } from 'ahooks'
import { AlertCircleIcon, Loader2Icon, SparklesIcon } from 'lucide-react'
import { sendMessage } from 'webext-bridge/popup'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@web-archive/shared/components/tooltip'
import toast from 'react-hot-toast'

async function getAllTags() {
const { tags } = await sendMessage('get-all-tags', {})
Expand All @@ -20,6 +21,10 @@ async function getAITagConfig() {
}

async function doGenerateTag(props: GenerateTagProps) {
if (!props.model) {
toast.error('Please configure in website settings first')
throw new Error('Invalid AI tag config')
}
if (props.type === 'cloudflare') {
const { tags } = await sendMessage('generate-tag', props)
return tags
Expand Down
6 changes: 3 additions & 3 deletions packages/plugin/popup/components/UploadPageForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ function UploadPageForm({ setActivePage }: UploadPageFormProps) {
},
)

function handleCancle() {
function handleCancel() {
setActivePage('home')
}

Expand Down Expand Up @@ -132,7 +132,7 @@ function UploadPageForm({ setActivePage }: UploadPageFormProps) {
}

return (
<div className="w-80 p-4 space-y-4 flex flex-col">
<div className="w-80 max-h-[600px] p-4 space-y-4 flex flex-col scrollbar-hide overflow-auto">
<div className="flex flex-col space-y-2">
<Label
htmlFor="title"
Expand Down Expand Up @@ -205,7 +205,7 @@ function UploadPageForm({ setActivePage }: UploadPageFormProps) {

<div className="flex justify-between">
<Button
onClick={handleCancle}
onClick={handleCancel}
variant="outline"
>
Cancel
Expand Down
41 changes: 37 additions & 4 deletions packages/server/src/api/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Hono } from 'hono'
import { validator } from 'hono/validator'
import type { AITagConfig } from '@web-archive/shared/types'
import { z } from 'zod'
import type { HonoTypeUserInformation } from '~/constants/binding'
import { getAITagConfig, getShouldShowRecent, setAITagConfig, setShouldShowRecent } from '~/model/store'
import result from '~/utils/result'
Expand Down Expand Up @@ -56,12 +57,44 @@ app.get('/ai_tag', async (c) => {
app.post(
'/ai_tag',
validator('json', (value, c) => {
// todo validate
if (typeof value !== 'object') {
return c.json(result.error(400, 'aiTagConfig is required'))
const modelError = {
message: 'Model name is required',
}
const apiUrlError = {
message: 'API URL is required',
}
const apiKeyError = {
message: 'API Key is required',
}
const cloudflareSchema = z.object({
type: z.literal('cloudflare'),
tagLanguage: z.enum(['en', 'zh']).default('en'),
model: z.string(modelError).min(1, modelError),
preferredTags: z.array(z.string()).default([]),
})
const openaiSchema = z.object({
type: z.literal('openai'),
tagLanguage: z.enum(['en', 'zh']).default('en'),
model: z.string(modelError).min(1, modelError),
preferredTags: z.array(z.string()).default([]),
apiUrl: z.string(apiUrlError).min(1, apiUrlError),
apiKey: z.string(apiKeyError).min(1, apiKeyError),
})

const schema = z.discriminatedUnion('type', [
cloudflareSchema,
openaiSchema,
])
const parsed = schema.safeParse(value)
if (!parsed.success) {
if (parsed.error.errors.length > 0) {
return c.json(result.error(400, parsed.error.errors[0].message))
}
return c.json(result.error(400, 'Invalid request'))
}

return value as AITagConfig
// todo set tsconfig strict to avoid type assertion
return parsed.data as AITagConfig
}),
async (c) => {
const aiTagConfig = c.req.valid('json')
Expand Down
31 changes: 23 additions & 8 deletions packages/server/src/api/tags.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { buildGenerateTagMessage, isNil, isNumberString } from '@web-archive/shared/utils'
import { Hono } from 'hono'
import { validator } from 'hono/validator'
import { z } from 'zod'
import type { HonoTypeUserInformation } from '~/constants/binding'
import { deleteTagById, insertTag, selectAllTags, updateTag } from '~/model/tag'
import result from '~/utils/result'
Expand Down Expand Up @@ -87,20 +88,28 @@ app.delete(
app.post(
'/generate_tag',
validator('json', (value, c) => {
// todo
return {
title: value.title,
pageDesc: value.pageDesc,
model: value.model,
tagLanguage: value.tagLanguage,
preferredTags: value.preferredTags,
const schema = z.object({
title: z.string({ message: 'Title is required' }).min(1, { message: 'Title is required' }),
pageDesc: z.string().default(''),
model: z.string({ message: 'Model name is required' }).min(1, { message: 'Model name is required' }),
tagLanguage: z.enum(['en', 'zh'], { message: 'Invalid tag language' }),
preferredTags: z.array(z.string()).default([]),
})
const parsed = schema.safeParse(value)
if (!parsed.success) {
if (parsed.error.errors.length > 0) {
return c.json(result.error(400, parsed.error.errors[0].message))
}
return c.json(result.error(400, 'Invalid request'))
}
return parsed.data
}),
async (c) => {
const { title, pageDesc, model, tagLanguage, preferredTags } = c.req.valid('json')

try {
const res = await c.env.AI.run(
// @ts-expect-error use BaseAiTextGenerationModels to check model? or use type assertion?
model,
{
messages: buildGenerateTagMessage({ title, pageDesc, tagLanguage, preferredTags }),
Expand All @@ -111,6 +120,9 @@ app.post(
if (res instanceof ReadableStream) {
throw new TypeError('Failed to parse response stream')
}
if (res.response === undefined) {
throw new TypeError('Failed to parse response')
}
const { tags } = JSON.parse(res.response)
return c.json(result.success(tags))
}
Expand All @@ -120,7 +132,10 @@ app.post(
}
}
catch (error) {
return c.json(result.error(500, error.message))
if (error instanceof Error) {
return c.json(result.error(500, error.message))
}
return c.json(result.error(500, 'Failed to generate tags'))
}
},
)
Expand Down
4 changes: 3 additions & 1 deletion packages/shared/utils/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ function generateChatCompletion(tagLanguage: string, preferredTags: string[]): s
5. Return format must be: {"tags": ["tag1", "tag2", ...]}
6. Do not return any explanatory text
7. Please prioritize these tags and add other relevant tags based on the content: [${preferredTags.join(', ')}]
8. Keep tags concise and focused. Return no more than 5 tags in total
9. Select the most representative and important tags only
`
}

Expand Down Expand Up @@ -82,7 +84,7 @@ export async function generateTagByOpenAI(props: GenerateTagProps): Promise<Arra
const data = await res.json() as GenerateTagResponse
const content = data.choices[0].message.content
const tagJson = JSON.parse(content)
return tagJson.tags
return tagJson.tags.slice(0, 5)
}
catch (error) {
throw new Error('Failed to parse response')
Expand Down
6 changes: 5 additions & 1 deletion packages/web/src/components/ai-tag-setting-collapsible.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ function AITagSettingCollapsible() {
onSubmit={form.handleSubmit(setAITagConfigRun)}
className="space-y-4 p-2"
>
<FormDescription>
Use page title and description to generate tags. You can also set preferred tags to be included in the generated tags.
</FormDescription>

<FormField
control={form.control}
name="type"
Expand Down Expand Up @@ -217,7 +221,7 @@ function AITagSettingCollapsible() {
name="preferredTags"
render={({ field }) => (
<FormItem>
<FormLabel>Prefered Tags</FormLabel>
<FormLabel>Preferred Tags</FormLabel>
<FormControl>
<AutoCompleteTagInput
tags={tagCache ?? []}
Expand Down

0 comments on commit 0dc1d04

Please sign in to comment.