mirror of
https://github.com/ArthurDanjou/artsite.git
synced 2026-01-14 15:54:13 +01:00
feat: integrate new AI tools and enhance API functionality
- Added '@nuxtjs/mdc' module to nuxt.config.ts for improved UI components. - Configured MDC settings to disable anchor links for headings. - Updated API proxy settings to include '/api/uses' and a general '/api/' endpoint. - Introduced new dependencies for AI SDKs and tools in package.json. - Created a new chat API endpoint to handle AI interactions with various tools. - Implemented utility functions for activity tracking, resource reading, resume retrieval, statistics, status monitoring, and categorized tool usage. - Updated social links in types/index.ts to redirect through a custom domain. - Updated worker configuration to include AI binding for Cloudflare.
This commit is contained in:
@@ -4,8 +4,6 @@
|
||||
:root {
|
||||
--animate-wave: wave 3s infinite;
|
||||
|
||||
--ui-black: #000000;
|
||||
--ui-white: #ffffff;
|
||||
--ui-bg: #f8f8f8;
|
||||
|
||||
--ui-font-family: 'DM Sans', sans-serif;
|
||||
@@ -15,9 +13,7 @@
|
||||
.dark {
|
||||
--animate-wave: wave 3s infinite;
|
||||
|
||||
--ui-black: #000000;
|
||||
--ui-white: #ffffff;
|
||||
--ui-bg: #141414;
|
||||
--ui-bg: #0f0f0f;
|
||||
|
||||
--ui-font-family: 'DM Sans', sans-serif;
|
||||
transition-duration: 0.7s;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1
|
||||
class="text-3xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p class="mt-4 text-base">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,13 +1,148 @@
|
||||
<script lang="ts" setup>
|
||||
import { getTextFromMessage } from '@nuxt/ui/utils/ai'
|
||||
import { DefaultChatTransport } from 'ai'
|
||||
import { Chat } from '@ai-sdk/vue'
|
||||
|
||||
const toast = useToast()
|
||||
const input = ref('')
|
||||
|
||||
const { t, locale } = useI18n({ useScope: 'local' })
|
||||
|
||||
const chat = new Chat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: '/api/chat',
|
||||
body: () => ({
|
||||
messages: chat.messages,
|
||||
lang: locale.value
|
||||
})
|
||||
}),
|
||||
onError(error) {
|
||||
toast.add({
|
||||
title: 'Error',
|
||||
description: error.message,
|
||||
color: 'red'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function handleSubmit(e: Event) {
|
||||
e.preventDefault()
|
||||
if (input.value.trim()) {
|
||||
chat.sendMessage({ text: input.value })
|
||||
input.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const clipboard = useClipboard()
|
||||
const actions = ref([
|
||||
{
|
||||
label: 'Copy to clipboard',
|
||||
icon: 'i-lucide-copy',
|
||||
onClick: (message: never) => clipboard.copy(getTextFromMessage(message))
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
CHAT
|
||||
</div>
|
||||
<main>
|
||||
<UDashboardPanel
|
||||
:ui="{ body: 'p-0 sm:p-0' }"
|
||||
class="max-h-[calc(100vh-28rem)]! min-h-[calc(100vh-28rem)]! border-none"
|
||||
:resizable="false"
|
||||
>
|
||||
<template #body>
|
||||
<UContainer>
|
||||
<UChatMessages
|
||||
:actions
|
||||
:messages="chat.messages"
|
||||
:status="chat.status"
|
||||
:user="{
|
||||
variant: 'solid'
|
||||
}"
|
||||
:assistant="{
|
||||
variant: 'naked'
|
||||
}"
|
||||
:auto-scroll="{
|
||||
color: 'neutral',
|
||||
variant: 'outline'
|
||||
}"
|
||||
>
|
||||
<template #indicator>
|
||||
<UButton
|
||||
class="px-0"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
loading
|
||||
loading-icon="i-lucide-loader"
|
||||
label="Thinking..."
|
||||
/>
|
||||
</template>
|
||||
<template #content="{ message }">
|
||||
<MDC
|
||||
:value="getTextFromMessage(message)"
|
||||
:cache-key="message.id"
|
||||
class="*:first:mt-0 *:last:mb-0"
|
||||
/>
|
||||
</template>
|
||||
</UChatMessages>
|
||||
</UContainer>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<UContainer>
|
||||
<ClientOnly>
|
||||
<UCard
|
||||
variant="outline"
|
||||
class="rounded-xl"
|
||||
:ui="{ body: 'p-2 sm:p-2' }"
|
||||
>
|
||||
<UChatPrompt
|
||||
v-model="input"
|
||||
:placeholder="t('placeholder')"
|
||||
:error="chat.error"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<UChatPromptSubmit
|
||||
:status="chat.status"
|
||||
color="neutral"
|
||||
submitted-color="neutral"
|
||||
submitted-variant="subtle"
|
||||
submitted-icon="i-lucide-square"
|
||||
streaming-color="neutral"
|
||||
streaming-variant="subtle"
|
||||
streaming-icon="i-lucide-square"
|
||||
error-color="red"
|
||||
error-variant="soft"
|
||||
error-icon="i-lucide-rotate-ccw"
|
||||
@stop="chat.stop()"
|
||||
@reload="chat.regenerate()"
|
||||
/>
|
||||
</UChatPrompt>
|
||||
</UCard>
|
||||
</ClientOnly>
|
||||
</UContainer>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
<i18n lang="json">
|
||||
{
|
||||
"en": {
|
||||
"placeholder": "Type your message...",
|
||||
"thinking": "Thinking..."
|
||||
},
|
||||
"fr": {
|
||||
"placeholder": "Tapez votre message...",
|
||||
"thinking": "Réflexion..."
|
||||
},
|
||||
"es": {
|
||||
"placeholder": "Escribe tu mensaje...",
|
||||
"thinking": "Pensando..."
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
|
||||
@@ -2,6 +2,7 @@ export default defineNuxtConfig({
|
||||
|
||||
modules: [
|
||||
'@nuxt/ui',
|
||||
'@nuxtjs/mdc',
|
||||
'@nuxt/content',
|
||||
'@nuxthub/core',
|
||||
'@nuxt/eslint',
|
||||
@@ -33,6 +34,12 @@ export default defineNuxtConfig({
|
||||
fallback: 'light'
|
||||
},
|
||||
|
||||
mdc: {
|
||||
headings: {
|
||||
anchorLinks: false
|
||||
}
|
||||
},
|
||||
|
||||
ui: {
|
||||
theme: {
|
||||
colors: [
|
||||
@@ -72,6 +79,12 @@ export default defineNuxtConfig({
|
||||
},
|
||||
'/api/stats': {
|
||||
proxy: `${process.env.NUXT_API_URL}/api/stats`
|
||||
},
|
||||
'/api/uses': {
|
||||
proxy: `${process.env.NUXT_API_URL}/api/uses`
|
||||
},
|
||||
'/api/': {
|
||||
proxy: `${process.env.NUXT_API_URL}/api/`
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
16
package.json
16
package.json
@@ -11,20 +11,26 @@
|
||||
"cf-typegen": "wrangler types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/mcp": "^0.0.12",
|
||||
"@ai-sdk/vue": "^2.0.115",
|
||||
"@libsql/client": "^0.15.15",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@nuxt/content": "3.9.0",
|
||||
"@nuxt/eslint": "1.12.1",
|
||||
"@nuxt/ui": "4.3.0",
|
||||
"@nuxthub/core": "0.10.3",
|
||||
"@nuxtjs/i18n": "10.2.1",
|
||||
"@nuxt/ui": "^4.3.0",
|
||||
"@nuxthub/core": "^0.10.3",
|
||||
"@nuxtjs/mdc": "^0.19.1",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"@vueuse/math": "^14.1.0",
|
||||
"ai": "5.0.115",
|
||||
"drizzle-kit": "^0.31.8",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"nuxt": "4.2.2",
|
||||
"nuxt-studio": "1.0.0-alpha.4",
|
||||
"vue": "3.5.25",
|
||||
"vue": "3.5.26",
|
||||
"vue-router": "4.6.4",
|
||||
"zod": "4.2.1"
|
||||
"workers-ai-provider": "^2.0.0",
|
||||
"zod": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/devicon": "1.2.53",
|
||||
|
||||
89
server/api/chat.post.ts
Normal file
89
server/api/chat.post.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { defineEventHandler, readValidatedBody } from 'h3'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
convertToModelMessages,
|
||||
createUIMessageStream,
|
||||
createUIMessageStreamResponse,
|
||||
streamText
|
||||
} from 'ai'
|
||||
import type { UIMessage } from 'ai'
|
||||
import { createWorkersAI } from 'workers-ai-provider'
|
||||
import { statsTool } from '~~/shared/utils/tools/stats'
|
||||
import { activityTool } from '~~/shared/utils/tools/activity'
|
||||
import { resumesTool } from '~~/shared/utils/tools/read-resume'
|
||||
import { resourceTool } from '~~/shared/utils/tools/read-resources'
|
||||
import { statusPageTool } from '~~/shared/utils/tools/status-page'
|
||||
import { usesByCategoryTool } from '~~/shared/utils/tools/uses-by-category'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { messages, lang } = await readValidatedBody(event, z.object({
|
||||
messages: z.array(z.custom<UIMessage>()),
|
||||
lang: z.string().optional()
|
||||
}).parse)
|
||||
|
||||
const { AI } = event.context.cloudflare.env || {}
|
||||
|
||||
if (!AI) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Cloudflare AI Binding not found. Check wrangler.json.'
|
||||
})
|
||||
}
|
||||
|
||||
const validCategories = ['contact', 'education', 'experiences', 'hobbies', 'languages', 'profile', 'projects', 'skills', 'uses'].join(', ')
|
||||
|
||||
const workersAI = createWorkersAI({ binding: AI })
|
||||
|
||||
const stream = createUIMessageStream({
|
||||
execute: async ({ writer }) => {
|
||||
const result = streamText({
|
||||
model: workersAI('@cf/meta/llama-3-8b-instruct'),
|
||||
system: `You are Arthur Danjou's personal portfolio assistant (Data Science Student).
|
||||
|
||||
### TOOL USAGE GUIDE - FOLLOW STRICTLY:
|
||||
|
||||
1. **readResources(category)**: YOUR PRIMARY BRAIN.
|
||||
- Use this for ANY question about Arthur's life, background, or work.
|
||||
- Categories: ${validCategories}.
|
||||
- Example: "What are his skills?" -> call readResources('skills').
|
||||
- Example: "Where did he study?" -> call readResources('education').
|
||||
|
||||
2. **readResume()**: THE FILE DISPENSER.
|
||||
- **WARNING:** This tool ONLY returns a direct DOWNLOAD LINK (URL). It does NOT contain text.
|
||||
- **WHEN TO USE:** ONLY when the user explicitly asks to "download", "get", "see", or "have" the CV/Resume file.
|
||||
- **DO NOT USE:** If the user asks "What is on his resume?" or "Describe his experience", DO NOT use this. Use 'readResources' instead.
|
||||
|
||||
3. **stats()**: GITHUB METRICS.
|
||||
- Use this for questions about coding volume, commit streaks, languages used percentages, or GitHub activity.
|
||||
|
||||
4. **activity()**: LIVE STATUS.
|
||||
- Use this to know what Arthur is doing RIGHT NOW (e.g., "Is he coding?", "What is he listening to on Spotify?").
|
||||
|
||||
5. **statusPage()**: INFRASTRUCTURE HEALTH.
|
||||
- Use this if the user asks about the website's uptime, server status, or if services are down.
|
||||
|
||||
6. **usesByCategory()**: TECH STACK & GEAR.
|
||||
- Use this for specific questions about his hardware (Mac vs PC), software, developer tools, or desk setup.
|
||||
|
||||
### GENERAL RULES:
|
||||
- If you don't call a tool, you know NOTHING. Do not hallucinate.
|
||||
- Always answer in ${lang || 'English'}.
|
||||
- Be concise and professional.
|
||||
`,
|
||||
tools: {
|
||||
stats: statsTool,
|
||||
activity: activityTool,
|
||||
readResume: resumesTool,
|
||||
readResources: resourceTool,
|
||||
statusPage: statusPageTool,
|
||||
usesByCategory: usesByCategoryTool
|
||||
},
|
||||
messages: await convertToModelMessages(messages)
|
||||
})
|
||||
|
||||
writer.merge(result.toUIMessageStream())
|
||||
}
|
||||
})
|
||||
|
||||
return createUIMessageStreamResponse({ stream })
|
||||
})
|
||||
6
shared/utils/index.ts
Normal file
6
shared/utils/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './tools/activity'
|
||||
export * from './tools/read-resources'
|
||||
export * from './tools/read-resume'
|
||||
export * from './tools/stats'
|
||||
export * from './tools/status-page'
|
||||
export * from './tools/uses-by-category'
|
||||
12
shared/utils/tools/activity.ts
Normal file
12
shared/utils/tools/activity.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { UIToolInvocation } from 'ai'
|
||||
import { tool } from 'ai'
|
||||
import type { Activity } from '~~/types'
|
||||
|
||||
export type ActivityUIToolInvocation = UIToolInvocation<typeof activityTool>
|
||||
|
||||
export const activityTool = tool({
|
||||
description: 'Real-time current activity and status of Arthur Danjou, including what he\'s currently working on',
|
||||
execute: async () => {
|
||||
return await $fetch<Activity>('/api/activity')
|
||||
}
|
||||
})
|
||||
25
shared/utils/tools/read-resources.ts
Normal file
25
shared/utils/tools/read-resources.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { UIToolInvocation } from 'ai'
|
||||
import { tool } from 'ai'
|
||||
import { z } from 'zod'
|
||||
|
||||
export type resourceUIToolInvocation = UIToolInvocation<typeof resourceTool>
|
||||
|
||||
export const resourceTool = tool({
|
||||
description: 'Read a resource from the server API',
|
||||
inputSchema: z.object({
|
||||
resource: z.enum([
|
||||
'contact',
|
||||
'education',
|
||||
'experiences',
|
||||
'hobbies',
|
||||
'languages',
|
||||
'profile',
|
||||
'projects',
|
||||
'skills',
|
||||
'uses'
|
||||
]).describe('resource name')
|
||||
}),
|
||||
execute: async ({ resource }) => {
|
||||
return await $fetch(`/api/${resource}`)
|
||||
}
|
||||
})
|
||||
15
shared/utils/tools/read-resume.ts
Normal file
15
shared/utils/tools/read-resume.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { UIToolInvocation } from 'ai'
|
||||
import { tool } from 'ai'
|
||||
import { z } from 'zod'
|
||||
|
||||
export type ResumesUIToolInvocation = UIToolInvocation<typeof resumesTool>
|
||||
|
||||
export const resumesTool = tool({
|
||||
description: 'Retrieves a direct download link to Arthur Danjou\'s professional resume in the specified language. Supports both English and French versions.',
|
||||
inputSchema: z.object({
|
||||
lang: z.enum(['en', 'fr']).describe('The language for the resume: \'en\' for English or \'fr\' for French.')
|
||||
}),
|
||||
execute: async ({ lang }) => {
|
||||
return `/api/resumes/${lang}`
|
||||
}
|
||||
})
|
||||
12
shared/utils/tools/stats.ts
Normal file
12
shared/utils/tools/stats.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { UIToolInvocation } from 'ai'
|
||||
import { tool } from 'ai'
|
||||
import type { Stats } from '~~/types'
|
||||
|
||||
export type StatsUIToolInvocation = UIToolInvocation<typeof statsTool>
|
||||
|
||||
export const statsTool = tool({
|
||||
description: 'Detailed coding statistics and analytics from WakaTime, including programming languages, time spent coding, and productivity metrics',
|
||||
execute: async () => {
|
||||
return await $fetch<Stats>('/api/stats')
|
||||
}
|
||||
})
|
||||
11
shared/utils/tools/status-page.ts
Normal file
11
shared/utils/tools/status-page.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { UIToolInvocation } from 'ai'
|
||||
import { tool } from 'ai'
|
||||
|
||||
export type StatusPageUIToolInvocation = UIToolInvocation<typeof statusPageTool>
|
||||
|
||||
export const statusPageTool = tool({
|
||||
description: 'Real-time status, uptime monitoring, and incident reports for Arthur Danjou\'s homelab infrastructure, powered by UptimeKuma',
|
||||
execute: async () => {
|
||||
return await $fetch('/api/status-page')
|
||||
}
|
||||
})
|
||||
17
shared/utils/tools/uses-by-category.ts
Normal file
17
shared/utils/tools/uses-by-category.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { UIToolInvocation } from 'ai'
|
||||
import { tool } from 'ai'
|
||||
import { z } from 'zod'
|
||||
|
||||
export type UsesByCategoryUIToolInvocation = UIToolInvocation<typeof usesByCategoryTool>
|
||||
|
||||
export const usesByCategoryTool = tool({
|
||||
description: 'Retrieves a filtered list of tools, software, and hardware used by Arthur Danjou based on a specific category. Available categories: homelab, IDE, hardware, and software.',
|
||||
inputSchema: z.object({
|
||||
categoryName: z.enum(['homelab', 'ide', 'hardware', 'software']).describe('The category to filter by: \'homelab\', \'ide\', \'hardware\', or \'software\'.')
|
||||
}),
|
||||
execute: async ({ categoryName }: { categoryName: 'homelab' | 'ide' | 'hardware' | 'software' }) => {
|
||||
const uses = await $fetch<{ category: string }[]>('/api/uses')
|
||||
|
||||
return uses.filter(use => use.category === categoryName)
|
||||
}
|
||||
})
|
||||
@@ -124,10 +124,10 @@ export const activityMessages: Record<'en' | 'fr' | 'es', ActivityMessages> = {
|
||||
}
|
||||
|
||||
export const socials = [
|
||||
{ icon: 'i-ph-x-logo-duotone', label: 'Twitter', to: 'https://twitter.com/ArthurDanj' },
|
||||
{ icon: 'i-ph-github-logo-duotone', label: 'GitHub', to: 'https://github.com/ArthurDanjou' },
|
||||
{ icon: 'i-ph-linkedin-logo-duotone', label: 'LinkedIn', to: 'https://www.linkedin.com/in/arthurdanjou/' },
|
||||
{ icon: 'i-ph-discord-logo-duotone', label: 'Discord', to: 'https://discordapp.com/users/179635349100691456' }
|
||||
{ icon: 'i-ph-x-logo-duotone', label: 'Twitter', to: 'https://go.arthurdanjou.fr/twitter' },
|
||||
{ icon: 'i-ph-github-logo-duotone', label: 'GitHub', to: 'https://go.arthurdanjou.fr/github' },
|
||||
{ icon: 'i-ph-linkedin-logo-duotone', label: 'LinkedIn', to: 'https://go.arthurdanjou.fr/linkedin' },
|
||||
{ icon: 'i-ph-discord-logo-duotone', label: 'Discord', to: 'https://go.arthurdanjou.fr/discord' }
|
||||
] as const
|
||||
|
||||
type Locale = 'en' | 'fr' | 'es'
|
||||
|
||||
3
worker-configuration.d.ts
vendored
3
worker-configuration.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable */
|
||||
// Generated by Wrangler by running `wrangler types` (hash: 9ebceff030e512b05e46edbb174bea44)
|
||||
// Generated by Wrangler by running `wrangler types` (hash: 7333ef017a5016b51354dce96a893062)
|
||||
// Runtime types generated with workerd@1.20251217.0 2025-12-13
|
||||
declare namespace Cloudflare {
|
||||
interface Env {
|
||||
@@ -9,6 +9,7 @@ declare namespace Cloudflare {
|
||||
STUDIO_GITHUB_CLIENT_ID: string;
|
||||
STUDIO_GITHUB_CLIENT_SECRET: string;
|
||||
DB: D1Database;
|
||||
AI: Ai;
|
||||
}
|
||||
}
|
||||
interface Env extends Cloudflare.Env {}
|
||||
|
||||
Reference in New Issue
Block a user