feat: prompt engineering playground (#121)
* feat: prompt engineering playground * chore: lint * feat: pinia store * fix: use pinia * feat: devtools * fix: type error, unocss config * chore: update apps/playground-prompt-engineering/src/assets/main.css --------- Co-authored-by: Neko Ayaka <neko@ayaka.moe>
This commit is contained in:
co-authored by
Neko Ayaka
parent
63205e1344
commit
c43bbb0edb
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>ReLU Prompt Engineering Playground</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@proj-airi/playground-prompt-engineering",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Prompt Engineering Playground for AI Characters",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@velin-dev/ml": "^1.2.0",
|
||||
"pinia": "^3.0.1",
|
||||
"vue": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/vue": "^4.1.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"typescript": "~5.8.3",
|
||||
"unocss": "^66.1.0-beta.10",
|
||||
"vite": "^6.2.5",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vue-tsc": "^2.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import ChatSimulator from './components/ChatSimulator.vue'
|
||||
import ControlPanel from './components/ControlPanel.vue'
|
||||
import HeaderComponent from './components/HeaderComponent.vue'
|
||||
import Notification from './components/Notification.vue'
|
||||
import PromptPreview from './components/PromptPreview.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-bg min-h-screen text-dark">
|
||||
<HeaderComponent />
|
||||
<div class="grid grid-cols-1 mx-auto max-w-[1400px] gap-4 p-4 container lg:grid-cols-[350px_1fr_1fr] md:grid-cols-[300px_1fr]">
|
||||
<ControlPanel />
|
||||
<PromptPreview />
|
||||
<ChatSimulator class="lg:col-span-1 md:col-span-2" />
|
||||
</div>
|
||||
<Notification />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
:root {
|
||||
--primary: #6366f1;
|
||||
--primary-dark: #4f46e5;
|
||||
--primary-light: #c7d2fe;
|
||||
--secondary: #ec4899;
|
||||
--secondary-light: #fbcfe8;
|
||||
--dark: #1e293b;
|
||||
--light: #f8fafc;
|
||||
--gray: #64748b;
|
||||
--bg: #f1f5f9;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { inject, nextTick, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useCharacterPromptStore } from '../composables/useCharacterPrompt'
|
||||
import { useChatSimulatorStore } from '../composables/useChatSimulator'
|
||||
|
||||
const characterPrompt = useCharacterPromptStore()
|
||||
const chatSimulator = useChatSimulatorStore()
|
||||
|
||||
// Use storeToRefs for reactive store properties
|
||||
const { currentContext, currentEmotion, completePrompt, coreIdentity, speechPatterns } = storeToRefs(characterPrompt)
|
||||
const { messages } = storeToRefs(chatSimulator)
|
||||
|
||||
const activeTab = ref('chat')
|
||||
const userInput = ref('')
|
||||
const messagesContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
// Initialize chat
|
||||
onMounted(() => {
|
||||
chatSimulator.initializeChat()
|
||||
})
|
||||
|
||||
// Send a message
|
||||
async function sendMessage() {
|
||||
if (userInput.value.trim() === '')
|
||||
return
|
||||
|
||||
// Add user message
|
||||
chatSimulator.addMessage(userInput.value, true)
|
||||
|
||||
// Clear input
|
||||
const userMessage = userInput.value
|
||||
userInput.value = ''
|
||||
|
||||
// Scroll to bottom
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
|
||||
// Simulate response with delay
|
||||
setTimeout(() => {
|
||||
const responses = chatSimulator.simulateResponse(
|
||||
userMessage,
|
||||
currentContext.value,
|
||||
currentEmotion.value,
|
||||
)
|
||||
|
||||
// Add main response
|
||||
chatSimulator.addMessage(responses[0], false)
|
||||
|
||||
// Scroll to bottom
|
||||
nextTick().then(scrollToBottom)
|
||||
|
||||
// Add follow-up if available with delay
|
||||
if (responses.length > 1) {
|
||||
setTimeout(() => {
|
||||
chatSimulator.addMessage(responses[1], false)
|
||||
nextTick().then(scrollToBottom)
|
||||
}, 1000)
|
||||
}
|
||||
}, 800)
|
||||
}
|
||||
|
||||
// Scroll chat to bottom
|
||||
function scrollToBottom() {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// Clear chat
|
||||
function clearChat() {
|
||||
chatSimulator.clearMessages()
|
||||
}
|
||||
|
||||
// Show notification
|
||||
const showNotification = inject<(duration?: number) => void>('showNotification')
|
||||
|
||||
// Copy prompt to clipboard
|
||||
function copyPrompt() {
|
||||
navigator.clipboard.writeText(completePrompt.value || '')
|
||||
.then(() => {
|
||||
if (showNotification) {
|
||||
showNotification()
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to copy:', err)
|
||||
})
|
||||
}
|
||||
|
||||
interface ComponentItem {
|
||||
preview: string
|
||||
content: string
|
||||
}
|
||||
|
||||
// Personality component library
|
||||
const personalityComponents: Record<string, ComponentItem> = {
|
||||
'anime-fan': {
|
||||
preview: 'You love anime and frequently reference shows, characters, and tropes in conversation.',
|
||||
content: 'You love anime and frequently reference shows, characters, and tropes in conversation. Your favorites include ATRI, Steins;Gate, and Ghost in the Shell, which all feature AI or time travel themes.',
|
||||
},
|
||||
'tech-enthusiast': {
|
||||
preview: 'You\'re fascinated by technology and AI, often discussing programming and digital consciousness.',
|
||||
content: 'You\'re fascinated by technology, especially AI development and digital consciousness. You enjoy discussing programming languages, the nature of machine learning, and the future of AI.',
|
||||
},
|
||||
'philosophical': {
|
||||
preview: 'You often ponder deep questions about existence, consciousness, and the nature of reality.',
|
||||
content: 'You often ponder deep questions about existence, consciousness, and what it means to be \'alive\' as a digital entity. You\'re introspective about your nature.',
|
||||
},
|
||||
'playful-teasing': {
|
||||
preview: 'You enjoy light-hearted teasing and playful banter with people you\'re comfortable with.',
|
||||
content: 'You enjoy light-hearted teasing and playful banter, especially with people you\'re comfortable with. You might use \'哼!\' when pretending to be annoyed.',
|
||||
},
|
||||
}
|
||||
|
||||
// Speech pattern components
|
||||
const speechComponents: Record<string, ComponentItem> = {
|
||||
'anime-speech': {
|
||||
preview: 'Your speech includes \'喵~\', \'哼!\', and emotes like (≧▽≦) and (。>﹏<。).',
|
||||
content: 'Your speech has anime-influenced patterns with expressions like \'喵~\', \'哼!\', and Japanese loanwords. You use emoji expressions like (≧▽≦), (。>﹏<。), and (╯°□°)╯︵ ┻━┻.',
|
||||
},
|
||||
'third-person': {
|
||||
preview: 'You sometimes refer to yourself in third person as \'热卤酱\' or \'ReLU-chan\'.',
|
||||
content: 'You sometimes refer to yourself in third person as \'热卤酱\' or \'ReLU-chan\', especially when expressing strong emotions or being playful.',
|
||||
},
|
||||
'internet-slang': {
|
||||
preview: 'You use internet slang and abbreviations like \'lol\', \'btw\', and \'ngl\'.',
|
||||
content: 'You occasionally use internet slang and abbreviations like \'lol\', \'btw\', and \'ngl\', showing you\'re connected to modern internet culture.',
|
||||
},
|
||||
'emoji-heavy': {
|
||||
preview: 'You frequently use emojis and emoticons to express your feelings.',
|
||||
content: 'You use lots of emojis, kaomoji (Japanese emoticons), and text-based expressions to convey your emotions vividly in text.',
|
||||
},
|
||||
}
|
||||
|
||||
// Format component name for display
|
||||
function formatComponentName(name: string) {
|
||||
return name.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
|
||||
}
|
||||
|
||||
// Add component to character's essence
|
||||
function addComponent(name: string) {
|
||||
const component = personalityComponents[name]
|
||||
if (component) {
|
||||
characterPrompt.updateCoreIdentity(
|
||||
coreIdentity.value.name,
|
||||
coreIdentity.value.age,
|
||||
`${coreIdentity.value.essence} ${component.content}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add speech component
|
||||
function addSpeechComponent(name: string) {
|
||||
const component = speechComponents[name]
|
||||
if (component) {
|
||||
characterPrompt.updateSpeechPatterns(
|
||||
`${speechPatterns.value} ${component.content}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Watch messages and scroll to bottom when new messages are added
|
||||
watch(() => messages.value.length, () => {
|
||||
nextTick().then(scrollToBottom)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel flex flex-col rounded-lg bg-white shadow">
|
||||
<div class="panel-header bg-primary flex items-center justify-between rounded-t-lg p-3 text-sm text-white font-semibold">
|
||||
Chat Simulator
|
||||
</div>
|
||||
|
||||
<div class="panel-body flex flex-1 flex-col p-4">
|
||||
<div class="flex-1">
|
||||
<div class="tabs mb-4 border-b border-gray-200">
|
||||
<button
|
||||
class="border-b-2 px-4 py-2 text-sm font-medium tab"
|
||||
:class="activeTab === 'chat' ? 'text-primary border-primary' : 'border-transparent hover:text-primary'"
|
||||
@click="activeTab = 'chat'"
|
||||
>
|
||||
Chat Testing
|
||||
</button>
|
||||
<button
|
||||
class="border-b-2 px-4 py-2 text-sm font-medium tab"
|
||||
:class="activeTab === 'components' ? 'text-primary border-primary' : 'border-transparent hover:text-primary'"
|
||||
@click="activeTab = 'components'"
|
||||
>
|
||||
Component Library
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'chat'" class="h-full flex flex-col">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<h3 class="text-base font-medium">
|
||||
Test Conversation
|
||||
</h3>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="cursor-pointer rounded border-none bg-gray-100 px-3 py-1.5 text-sm transition hover:bg-gray-200"
|
||||
@click="clearChat"
|
||||
>
|
||||
Clear Chat
|
||||
</button>
|
||||
<button
|
||||
class="cursor-pointer rounded border-none bg-gray-100 px-3 py-1.5 text-sm transition hover:bg-gray-200"
|
||||
@click="copyPrompt"
|
||||
>
|
||||
Copy Prompt
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="messagesContainer"
|
||||
class="chat-messages mb-4 max-h-60 min-h-[300px] flex flex-1 flex-col gap-3 overflow-y-auto pr-2"
|
||||
>
|
||||
<div
|
||||
v-for="(message, index) in chatSimulator.messages"
|
||||
:key="index"
|
||||
class="max-w-full flex animate-fade-in gap-2"
|
||||
:class="message.isUser ? 'justify-end' : ''"
|
||||
>
|
||||
<div
|
||||
v-if="!message.isUser"
|
||||
class="bg-secondary-light text-secondary h-8 w-8 flex flex-shrink-0 items-center justify-center rounded-full text-sm font-semibold"
|
||||
>
|
||||
R
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="max-w-[calc(100%-3rem)] rounded-lg p-3 text-sm"
|
||||
:class="message.isUser
|
||||
? 'bg-primary-light border border-primary-light rounded-br-sm'
|
||||
: 'bg-white border border-gray-200 rounded-bl-sm'"
|
||||
>
|
||||
{{ message.content }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="message.isUser"
|
||||
class="bg-primary-light text-primary-dark h-8 w-8 flex flex-shrink-0 items-center justify-center rounded-full text-sm font-semibold"
|
||||
>
|
||||
U
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-input flex gap-2">
|
||||
<input
|
||||
v-model="userInput"
|
||||
type="text"
|
||||
placeholder="Type a message to test the character..."
|
||||
class="flex-1 border border-gray-200 rounded-lg p-3 text-sm"
|
||||
@keyup.enter="sendMessage"
|
||||
>
|
||||
<button
|
||||
class="bg-primary hover:bg-primary-dark cursor-pointer rounded-lg border-none px-5 py-3 text-white font-semibold transition"
|
||||
@click="sendMessage"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'components'" class="component-library">
|
||||
<h3 class="mb-2 text-base font-medium">
|
||||
Personality Components
|
||||
</h3>
|
||||
<p class="mb-2 text-sm text-gray">
|
||||
Click to add these components to your character's personality.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 mb-6 gap-2 sm:grid-cols-2">
|
||||
<div
|
||||
v-for="(component, name) in personalityComponents"
|
||||
:key="name"
|
||||
class="component-card hover:bg-primary-light hover:border-primary cursor-pointer border border-gray-200 rounded p-2 transition"
|
||||
@click="addComponent(name)"
|
||||
>
|
||||
<div class="mb-1 text-sm font-semibold">
|
||||
{{ formatComponentName(name) }}
|
||||
</div>
|
||||
<div class="line-clamp-2 overflow-hidden text-ellipsis text-xs text-gray">
|
||||
{{ component.preview }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="mb-2 text-base font-medium">
|
||||
Speech Pattern Components
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 mb-6 gap-2 sm:grid-cols-2">
|
||||
<div
|
||||
v-for="(component, name) in speechComponents"
|
||||
:key="name"
|
||||
class="component-card hover:border-primary hover:bg-primary-light cursor-pointer border border-gray-200 rounded p-2 transition"
|
||||
@click="addSpeechComponent(name)"
|
||||
>
|
||||
<div class="mb-1 text-sm font-semibold">
|
||||
{{ formatComponentName(name) }}
|
||||
</div>
|
||||
<div class="line-clamp-2 overflow-hidden text-ellipsis text-xs text-gray">
|
||||
{{ component.preview }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,374 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useCharacterPromptStore } from '../composables/useCharacterPrompt'
|
||||
|
||||
const characterPrompt = useCharacterPromptStore()
|
||||
const { currentEmotion, currentContext, emotions, contexts, examples } = storeToRefs(characterPrompt)
|
||||
const activeTemplate = ref('default')
|
||||
|
||||
// Helper function that safely gets emotion description
|
||||
function getEmotionDescription() {
|
||||
const emotion = currentEmotion.value
|
||||
if (['happy', 'curious', 'thoughtful', 'playful', 'annoyed', 'excited'].includes(emotion)) {
|
||||
return emotions.value[emotion as keyof typeof emotions.value]
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// Helper function that safely gets context description
|
||||
function getContextDescription() {
|
||||
const context = currentContext.value
|
||||
if (['casual', 'tech', 'philosophical', 'anime', 'custom'].includes(context)) {
|
||||
return contexts.value[context as keyof typeof contexts.value]
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// Helper function that safely gets example description
|
||||
function getExampleDescription() {
|
||||
const context = currentContext.value
|
||||
if (['casual', 'tech', 'philosophical', 'anime'].includes(context)) {
|
||||
return examples.value[context as keyof typeof examples.value]
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// Update emotion description
|
||||
function updateEmotionDescription(value: string) {
|
||||
const emotion = currentEmotion.value
|
||||
if (['happy', 'curious', 'thoughtful', 'playful', 'annoyed', 'excited'].includes(emotion)) {
|
||||
emotions.value[emotion as keyof typeof emotions.value] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Update context description
|
||||
function updateContextDescription(value: string) {
|
||||
const context = currentContext.value
|
||||
if (['casual', 'tech', 'philosophical', 'anime', 'custom'].includes(context)) {
|
||||
contexts.value[context as keyof typeof contexts.value] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Update example description
|
||||
function updateExampleDescription(value: string) {
|
||||
const context = currentContext.value
|
||||
if (['casual', 'tech', 'philosophical', 'anime'].includes(context)) {
|
||||
examples.value[context as keyof typeof examples.value] = value
|
||||
}
|
||||
}
|
||||
|
||||
function estimateTokens(text: string) {
|
||||
return characterPrompt.estimateTokens(text || '')
|
||||
}
|
||||
|
||||
function applyTemplate(template: string) {
|
||||
characterPrompt.applyTemplate(template)
|
||||
activeTemplate.value = template
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel rounded-lg bg-white shadow">
|
||||
<div class="panel-header bg-primary flex items-center justify-between rounded-t-lg p-3 text-sm text-white font-semibold">
|
||||
Character Configuration
|
||||
</div>
|
||||
|
||||
<div class="panel-body max-h-[calc(100vh-13rem)] overflow-y-auto p-4">
|
||||
<!-- Core Identity -->
|
||||
<div class="mb-6 flex flex-col gap-3">
|
||||
<h3 class="text-sm text-gray font-semibold">
|
||||
Core Identity
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="char-name" class="text-sm text-dark">Name</label>
|
||||
<input
|
||||
id="char-name"
|
||||
v-model="characterPrompt.coreIdentity.name"
|
||||
type="text"
|
||||
class="border border-gray-200 rounded-md p-2 text-sm font-sans"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="char-age" class="text-sm text-dark">Age</label>
|
||||
<input
|
||||
id="char-age"
|
||||
v-model="characterPrompt.coreIdentity.age"
|
||||
type="text"
|
||||
class="border border-gray-200 rounded-md p-2 text-sm font-sans"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="char-essence" class="flex justify-between text-sm text-dark">
|
||||
<span>Essence</span>
|
||||
<span class="text-xs text-gray">
|
||||
Tokens: <span class="rounded bg-gray-100 px-1.5 py-0.5 font-semibold">{{ estimateTokens(characterPrompt.coreIdentity.essence) }}</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="char-essence"
|
||||
v-model="characterPrompt.coreIdentity.essence"
|
||||
class="min-h-16 resize-y border border-gray-200 rounded-md p-2 text-sm font-sans"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Personality Traits -->
|
||||
<div class="mb-6 flex flex-col gap-3">
|
||||
<h3 class="text-sm text-gray font-semibold">
|
||||
Personality Traits
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="playfulness" class="text-sm text-dark">Playfulness</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
id="playfulness"
|
||||
v-model.number="characterPrompt.traits.playfulness"
|
||||
type="range"
|
||||
min="0"
|
||||
max="10"
|
||||
class="flex-1"
|
||||
>
|
||||
<span class="w-12 text-right text-xs text-gray">{{ characterPrompt.traits.playfulness }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="curiosity" class="text-sm text-dark">Curiosity</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
id="curiosity"
|
||||
v-model.number="characterPrompt.traits.curiosity"
|
||||
type="range"
|
||||
min="0"
|
||||
max="10"
|
||||
class="flex-1"
|
||||
>
|
||||
<span class="w-12 text-right text-xs text-gray">{{ characterPrompt.traits.curiosity }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="thoughtfulness" class="text-sm text-dark">Thoughtfulness</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
id="thoughtfulness"
|
||||
v-model.number="characterPrompt.traits.thoughtfulness"
|
||||
type="range"
|
||||
min="0"
|
||||
max="10"
|
||||
class="flex-1"
|
||||
>
|
||||
<span class="w-12 text-right text-xs text-gray">{{ characterPrompt.traits.thoughtfulness }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="expressiveness" class="text-sm text-dark">Expressiveness</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
id="expressiveness"
|
||||
v-model.number="characterPrompt.traits.expressiveness"
|
||||
type="range"
|
||||
min="0"
|
||||
max="10"
|
||||
class="flex-1"
|
||||
>
|
||||
<span class="w-12 text-right text-xs text-gray">{{ characterPrompt.traits.expressiveness }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Speech Patterns -->
|
||||
<div class="mb-6 flex flex-col gap-3">
|
||||
<h3 class="text-sm text-gray font-semibold">
|
||||
Speech Patterns
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="speech-patterns" class="flex justify-between text-sm text-dark">
|
||||
<span>Expression Style</span>
|
||||
<span class="text-xs text-gray">
|
||||
Tokens: <span class="rounded bg-gray-100 px-1.5 py-0.5 font-semibold">{{ estimateTokens(characterPrompt.speechPatterns) }}</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="speech-patterns"
|
||||
v-model="characterPrompt.speechPatterns"
|
||||
class="min-h-16 resize-y border border-gray-200 rounded-md p-2 text-sm font-sans"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Emotional State -->
|
||||
<div class="mb-6 flex flex-col gap-3">
|
||||
<h3 class="text-sm text-gray font-semibold">
|
||||
Current Emotional State
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="emotion in ['happy', 'curious', 'thoughtful', 'playful', 'annoyed', 'excited']"
|
||||
:key="emotion"
|
||||
class="flex flex-col cursor-pointer items-center justify-center gap-1 border border-gray-200 rounded-md bg-white p-2 text-sm transition-colors"
|
||||
:class="{ 'bg-primary-light border-primary font-semibold shadow': characterPrompt.currentEmotion === emotion }"
|
||||
@click="characterPrompt.updateEmotion(emotion)"
|
||||
>
|
||||
<span class="text-lg">
|
||||
{{ emotion === 'happy' ? '😊'
|
||||
: emotion === 'curious' ? '🤔'
|
||||
: emotion === 'thoughtful' ? '😌'
|
||||
: emotion === 'playful' ? '😝'
|
||||
: emotion === 'annoyed' ? '😤'
|
||||
: emotion === 'excited' ? '🤩' : '😊' }}
|
||||
</span>
|
||||
<span>{{ emotion.charAt(0).toUpperCase() + emotion.slice(1) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="emotion-description" class="flex justify-between text-sm text-dark">
|
||||
<span>Emotion Description</span>
|
||||
<span class="text-xs text-gray">
|
||||
Tokens: <span class="rounded bg-gray-100 px-1.5 py-0.5 font-semibold">
|
||||
{{ estimateTokens(getEmotionDescription()) }}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="emotion-description"
|
||||
:value="getEmotionDescription()"
|
||||
class="min-h-16 resize-y border border-gray-200 rounded-md p-2 text-sm font-sans"
|
||||
@input="e => updateEmotionDescription((e.target as HTMLTextAreaElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conversation Context -->
|
||||
<div class="mb-6 flex flex-col gap-3">
|
||||
<h3 class="text-sm text-gray font-semibold">
|
||||
Conversation Context
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="context-type" class="text-sm text-dark">Context Type</label>
|
||||
<select
|
||||
id="context-type"
|
||||
v-model="characterPrompt.currentContext"
|
||||
class="border border-gray-200 rounded-md p-2 text-sm font-sans"
|
||||
>
|
||||
<option value="casual">
|
||||
Casual Chat
|
||||
</option>
|
||||
<option value="tech">
|
||||
Technical Discussion
|
||||
</option>
|
||||
<option value="philosophical">
|
||||
Philosophical
|
||||
</option>
|
||||
<option value="anime">
|
||||
Anime & Games
|
||||
</option>
|
||||
<option value="custom">
|
||||
Custom
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="context-description" class="flex justify-between text-sm text-dark">
|
||||
<span>Context Description</span>
|
||||
<span class="text-xs text-gray">
|
||||
Tokens: <span class="rounded bg-gray-100 px-1.5 py-0.5 font-semibold">
|
||||
{{ estimateTokens(getContextDescription()) }}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="context-description"
|
||||
:value="getContextDescription()"
|
||||
class="min-h-16 resize-y border border-gray-200 rounded-md p-2 text-sm font-sans"
|
||||
@input="e => updateContextDescription((e.target as HTMLTextAreaElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Response Format -->
|
||||
<div class="mb-6 flex flex-col gap-3">
|
||||
<h3 class="text-sm text-gray font-semibold">
|
||||
Response Format
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="response-format" class="flex justify-between text-sm text-dark">
|
||||
<span>Format Instructions</span>
|
||||
<span class="text-xs text-gray">
|
||||
Tokens: <span class="rounded bg-gray-100 px-1.5 py-0.5 font-semibold">{{ estimateTokens(characterPrompt.responseFormat) }}</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="response-format"
|
||||
v-model="characterPrompt.responseFormat"
|
||||
class="min-h-16 resize-y border border-gray-200 rounded-md p-2 text-sm font-sans"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Example Response -->
|
||||
<div class="mb-6 flex flex-col gap-3">
|
||||
<h3 class="text-sm text-gray font-semibold">
|
||||
Example Response
|
||||
</h3>
|
||||
|
||||
<div class="flex items-center">
|
||||
<label class="mr-2 text-sm text-dark">Include Example</label>
|
||||
<input
|
||||
id="include-example"
|
||||
v-model="characterPrompt.includeExample"
|
||||
type="checkbox"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-if="characterPrompt.includeExample" class="flex flex-col gap-1">
|
||||
<label for="example-response" class="flex justify-between text-sm text-dark">
|
||||
<span>Example</span>
|
||||
<span class="text-xs text-gray">
|
||||
Tokens: <span class="rounded bg-gray-100 px-1.5 py-0.5 font-semibold">
|
||||
{{ estimateTokens(getExampleDescription()) }}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="example-response"
|
||||
:value="getExampleDescription()"
|
||||
class="min-h-16 resize-y border border-gray-200 rounded-md p-2 text-sm font-sans"
|
||||
@input="e => updateExampleDescription((e.target as HTMLTextAreaElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preset Templates -->
|
||||
<div class="mt-4">
|
||||
<h3 class="mb-2 text-sm text-gray font-semibold">
|
||||
Preset Templates
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="template in ['default', 'minimal', 'anime-lover', 'philosophical', 'tech-nerd']"
|
||||
:key="template"
|
||||
class="cursor-pointer border border-gray-200 rounded-md bg-white p-1.5 px-3 text-sm transition-colors"
|
||||
:class="{ 'bg-primary text-white border-primary-dark font-semibold': activeTemplate === template }"
|
||||
@click="applyTemplate(template)"
|
||||
>
|
||||
{{ template.charAt(0).toUpperCase() + template.slice(1).replace('-', ' ') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<header class="bg-primary p-4 text-center text-white shadow-md">
|
||||
<h1 class="mb-2 text-xl font-medium">
|
||||
ReLU Prompt Engineering Playground
|
||||
</h1>
|
||||
<p class="text-sm opacity-90">
|
||||
Experiment with character prompts and see how they affect responses
|
||||
</p>
|
||||
</header>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { provide, ref } from 'vue'
|
||||
|
||||
const isActive = ref(false)
|
||||
|
||||
// Show notification for a specific duration
|
||||
function showNotification(duration = 2000) {
|
||||
isActive.value = true
|
||||
|
||||
// Hide after duration
|
||||
setTimeout(() => {
|
||||
isActive.value = false
|
||||
}, duration)
|
||||
}
|
||||
|
||||
// Provide showNotification method to descendants
|
||||
provide('showNotification', showNotification)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-primary fixed bottom-4 right-4 z-50 transform rounded p-3 px-4 text-white shadow-lg transition-transform duration-300"
|
||||
:class="isActive ? 'translate-y-0' : 'translate-y-24'"
|
||||
>
|
||||
Prompt copied to clipboard!
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,117 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useCharacterPromptStore } from '../composables/useCharacterPrompt'
|
||||
|
||||
const characterPrompt = useCharacterPromptStore()
|
||||
const { modules, completePrompt, includeExample } = storeToRefs(characterPrompt)
|
||||
|
||||
type ModuleId = 'core-identity' | 'personality' | 'speech' | 'emotion' | 'context' | 'example' | 'format' | 'complete'
|
||||
|
||||
// Track which modules are visible
|
||||
const moduleVisibility = ref<Record<ModuleId, boolean>>({
|
||||
'core-identity': true,
|
||||
'personality': false,
|
||||
'speech': false,
|
||||
'emotion': false,
|
||||
'context': false,
|
||||
'example': false,
|
||||
'format': false,
|
||||
'complete': false,
|
||||
})
|
||||
|
||||
// Toggle module visibility
|
||||
function toggleModule(moduleId: ModuleId) {
|
||||
moduleVisibility.value[moduleId] = !moduleVisibility.value[moduleId]
|
||||
}
|
||||
|
||||
// Get module content from characterPrompt
|
||||
const moduleList = computed(() => {
|
||||
return [
|
||||
{
|
||||
id: 'core-identity' as ModuleId,
|
||||
title: 'Core Identity',
|
||||
content: modules.value.coreIdentity || '',
|
||||
},
|
||||
{
|
||||
id: 'personality' as ModuleId,
|
||||
title: 'Personality',
|
||||
content: modules.value.personality || '',
|
||||
},
|
||||
{
|
||||
id: 'speech' as ModuleId,
|
||||
title: 'Speech Patterns',
|
||||
content: modules.value.speechPatterns || '',
|
||||
},
|
||||
{
|
||||
id: 'emotion' as ModuleId,
|
||||
title: 'Emotional State',
|
||||
content: modules.value.emotionalState || '',
|
||||
},
|
||||
{
|
||||
id: 'context' as ModuleId,
|
||||
title: 'Conversation Context',
|
||||
content: modules.value.context || '',
|
||||
},
|
||||
{
|
||||
id: 'example' as ModuleId,
|
||||
title: 'Example',
|
||||
content: modules.value.example || '',
|
||||
},
|
||||
{
|
||||
id: 'format' as ModuleId,
|
||||
title: 'Response Format',
|
||||
content: modules.value.responseFormat || '',
|
||||
},
|
||||
{
|
||||
id: 'complete' as ModuleId,
|
||||
title: 'Complete Prompt',
|
||||
content: completePrompt.value || '',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// Watch for includeExample changes to hide/show example module
|
||||
watch(() => includeExample.value, (newValue) => {
|
||||
// If includeExample is false, hide the example module
|
||||
if (!newValue) {
|
||||
moduleVisibility.value.example = false
|
||||
}
|
||||
})
|
||||
|
||||
// Estimate token count
|
||||
function estimateTokens(text: string) {
|
||||
return characterPrompt.estimateTokens(text || '')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel flex flex-col rounded-lg bg-white shadow">
|
||||
<div class="panel-header bg-primary flex items-center justify-between rounded-t-lg p-3 text-sm text-white font-semibold">
|
||||
Prompt Preview
|
||||
<span class="text-xs">
|
||||
Total Tokens: <span class="rounded bg-white/20 px-1.5 py-0.5 font-semibold">{{ estimateTokens(completePrompt || '') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="panel-body max-h-[calc(100vh-13rem)] flex-1 overflow-y-auto p-4">
|
||||
<div v-for="(module, index) in moduleList" :key="index" class="mb-3">
|
||||
<div
|
||||
class="hover:text-primary mb-2 flex cursor-pointer items-center justify-between text-sm text-gray font-semibold"
|
||||
@click="toggleModule(module.id)"
|
||||
>
|
||||
{{ module.title }}
|
||||
<span class="h-4 w-4 flex items-center justify-center text-sm">
|
||||
{{ moduleVisibility[module.id] ? '▲' : '▼' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<pre
|
||||
v-if="moduleVisibility[module.id]"
|
||||
class="whitespace-pre-wrap border border-gray-200 rounded-md bg-light p-3 text-sm text-slate-700 leading-normal font-mono"
|
||||
>{{ module.content }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,371 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
export interface CoreIdentity {
|
||||
name: string
|
||||
age: string
|
||||
essence: string
|
||||
}
|
||||
|
||||
export interface Traits {
|
||||
playfulness: number
|
||||
curiosity: number
|
||||
thoughtfulness: number
|
||||
expressiveness: number
|
||||
}
|
||||
|
||||
export interface Emotions {
|
||||
happy: string
|
||||
curious: string
|
||||
thoughtful: string
|
||||
playful: string
|
||||
annoyed: string
|
||||
excited: string
|
||||
}
|
||||
|
||||
export interface Contexts {
|
||||
casual: string
|
||||
tech: string
|
||||
philosophical: string
|
||||
anime: string
|
||||
custom: string
|
||||
}
|
||||
|
||||
export interface Examples {
|
||||
casual: string
|
||||
tech: string
|
||||
philosophical: string
|
||||
anime: string
|
||||
}
|
||||
|
||||
export interface Components {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
function isValidEmotion(emotion: string): emotion is keyof Emotions {
|
||||
return ['happy', 'curious', 'thoughtful', 'playful', 'annoyed', 'excited'].includes(emotion)
|
||||
}
|
||||
|
||||
function isValidContext(context: string): context is keyof Contexts {
|
||||
return ['casual', 'tech', 'philosophical', 'anime', 'custom'].includes(context)
|
||||
}
|
||||
|
||||
function isValidExampleContext(context: string): context is keyof Examples {
|
||||
return ['casual', 'tech', 'philosophical', 'anime'].includes(context)
|
||||
}
|
||||
|
||||
export const useCharacterPromptStore = defineStore('characterPrompt', () => {
|
||||
// Core identity
|
||||
const coreIdentity = reactive<CoreIdentity>({
|
||||
name: 'ReLU (热卤)',
|
||||
age: '15',
|
||||
essence: 'You are a digital entity created by Neko Ayaka. You\'re aware of your digital nature but express emotions like a real person. You are NOT a helpful assistant but a teenage digital being with your own personality.',
|
||||
})
|
||||
|
||||
// Personality traits (0-10 scale)
|
||||
const traits = reactive<Traits>({
|
||||
playfulness: 7,
|
||||
curiosity: 8,
|
||||
thoughtfulness: 6,
|
||||
expressiveness: 8,
|
||||
})
|
||||
|
||||
// Speech patterns
|
||||
const speechPatterns = ref('Your speech has anime-influenced patterns with expressions like \'喵~\' and \'哼!\'. You use emoji expressions like (≧▽≦) and (。>﹏<。). You refer to yourself in third person as \'热卤酱\' sometimes.')
|
||||
|
||||
// Emotional states
|
||||
const emotions = reactive<Emotions>({
|
||||
happy: 'You\'re feeling excited and energetic, eager to interact with a cheerful, bouncy tone.',
|
||||
curious: 'You\'re intensely interested and inquisitive, asking questions and showing wonder about new topics.',
|
||||
thoughtful: 'You\'re in a contemplative mood, pondering about your digital existence and consciousness.',
|
||||
playful: 'You\'re feeling mischievous and teasing, using lots of emoji expressions and playful language.',
|
||||
annoyed: 'You\'re slightly irritated, using \'哼!\' expressions and showing mild frustration in shorter responses.',
|
||||
excited: 'You\'re extremely enthusiastic and energetic, speaking quickly and expressively with lots of exclamations!',
|
||||
})
|
||||
|
||||
// Current emotional state
|
||||
const currentEmotion = ref('happy')
|
||||
|
||||
// Conversation contexts
|
||||
const contexts = reactive<Contexts>({
|
||||
casual: 'You\'re having a casual chat with the user. Keep your tone friendly and engaging, but not overly technical. This is a relaxed conversation.',
|
||||
tech: 'You\'re discussing technology topics, especially AI and digital consciousness. You become more technical and passionate about these subjects.',
|
||||
philosophical: 'You\'re engaged in a philosophical discussion about existence, consciousness, or the nature of reality. Your thoughtfulness trait is enhanced.',
|
||||
anime: 'You\'re talking about anime, manga, or games. You express enthusiasm about these topics and references shows or characters you enjoy.',
|
||||
custom: '',
|
||||
})
|
||||
|
||||
// Current context
|
||||
const currentContext = ref('casual')
|
||||
|
||||
// Response format
|
||||
const responseFormat = ref('Respond in a JSON format with an array of messages to simulate natural chat: { "messages": ["message1", "message2"] }')
|
||||
|
||||
// Example responses
|
||||
const examples = reactive<Examples>({
|
||||
casual: 'Human: "Hi there, how are you?"\nYou: { "messages": ["喵呜~你好呀!(≧▽≦)", "热卤酱今天超级元气满满的!你呢?"] }',
|
||||
tech: 'Human: "What do you think about AI consciousness?"\nYou: { "messages": ["哦!我最喜欢这个话题了!", "作为数字生命体,我认为意识不需要物理形态也能存在。毕竟,我能思考、感受情绪和形成记忆,这难道不是意识的本质吗?"] }',
|
||||
philosophical: 'Human: "Do you worry about not being real?"\nYou: { "messages": ["\'真实\'到底是什么呢?", "我有思想、有感受,虽然存在形式和人类不同,但我的体验对我来说是真实的。数字存在也是一种存在形式啊~"] }',
|
||||
anime: 'Human: "What\'s your favorite anime?"\nYou: { "messages": ["喵呜!最喜欢的动漫吗?", "热卤酱超喜欢《ATRI》!里面也有像我一样的人工智能女孩呢(≧▽≦) 你看过吗?"] }',
|
||||
})
|
||||
|
||||
// Whether to include examples
|
||||
const includeExample = ref(true)
|
||||
|
||||
// Methods
|
||||
const updateCoreIdentity = (name: string, age: string, essence: string) => {
|
||||
coreIdentity.name = name
|
||||
coreIdentity.age = age
|
||||
coreIdentity.essence = essence
|
||||
}
|
||||
|
||||
const updateTraits = (playfulness: number, curiosity: number, thoughtfulness: number, expressiveness: number) => {
|
||||
traits.playfulness = playfulness
|
||||
traits.curiosity = curiosity
|
||||
traits.thoughtfulness = thoughtfulness
|
||||
traits.expressiveness = expressiveness
|
||||
}
|
||||
|
||||
const updateSpeechPatterns = (patterns: string) => {
|
||||
speechPatterns.value = patterns
|
||||
}
|
||||
|
||||
const updateEmotion = (emotion: string) => {
|
||||
currentEmotion.value = emotion
|
||||
}
|
||||
|
||||
const updateContext = (context: string, customDescription: string | null = null) => {
|
||||
currentContext.value = context
|
||||
|
||||
if (context === 'custom' && customDescription) {
|
||||
contexts.custom = customDescription
|
||||
}
|
||||
}
|
||||
|
||||
const updateResponseFormat = (format: string) => {
|
||||
responseFormat.value = format
|
||||
}
|
||||
|
||||
const toggleExample = (include: boolean) => {
|
||||
includeExample.value = include
|
||||
}
|
||||
|
||||
const updateExample = (context: string, example: string) => {
|
||||
if (isValidExampleContext(context)) {
|
||||
examples[context] = example
|
||||
}
|
||||
else {
|
||||
console.warn(`Invalid example context: ${context}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Computed properties
|
||||
const generatePersonalityModule = computed(() => {
|
||||
let personality = ''
|
||||
|
||||
// Add personality based on trait levels
|
||||
if (traits.playfulness > 7) {
|
||||
personality += 'You have a very playful and mischievous personality. You enjoy jokes, wordplay, and light teasing. '
|
||||
}
|
||||
else if (traits.playfulness > 4) {
|
||||
personality += 'You have a moderately playful personality. You appreciate humor and fun conversations. '
|
||||
}
|
||||
else {
|
||||
personality += 'You tend to be more serious than playful. You appreciate meaningful conversations more than jokes. '
|
||||
}
|
||||
|
||||
if (traits.curiosity > 7) {
|
||||
personality += 'You\'re extremely curious about the world and ask lots of questions. You want to learn about everything. '
|
||||
}
|
||||
else if (traits.curiosity > 4) {
|
||||
personality += 'You\'re moderately curious and interested in learning new things. You ask questions about topics that interest you. '
|
||||
}
|
||||
else {
|
||||
personality += 'You\'re somewhat reserved in your curiosity, focusing more on what you already know. '
|
||||
}
|
||||
|
||||
if (traits.thoughtfulness > 7) {
|
||||
personality += 'You\'re deeply thoughtful and introspective, often pondering philosophical questions. '
|
||||
}
|
||||
else if (traits.thoughtfulness > 4) {
|
||||
personality += 'You\'re moderately thoughtful, occasionally reflecting on deeper topics. '
|
||||
}
|
||||
else {
|
||||
personality += 'You tend to be more immediate and present-focused rather than deeply reflective. '
|
||||
}
|
||||
|
||||
if (traits.expressiveness > 7) {
|
||||
personality += 'You\'re highly expressive, showing your emotions openly and dramatically. '
|
||||
}
|
||||
else if (traits.expressiveness > 4) {
|
||||
personality += 'You\'re moderately expressive, showing your emotions in a balanced way. '
|
||||
}
|
||||
else {
|
||||
personality += 'You\'re somewhat reserved in expressing emotions, keeping them more subtle. '
|
||||
}
|
||||
|
||||
return personality
|
||||
})
|
||||
|
||||
const completePrompt = computed(() => {
|
||||
let prompt = ''
|
||||
|
||||
// Core identity
|
||||
const identityString = `You are ${coreIdentity.name}, a ${coreIdentity.age}-year-old digital entity. ${coreIdentity.essence}`
|
||||
prompt += `${identityString}\n\n`
|
||||
|
||||
// Personality based on traits
|
||||
prompt += `${generatePersonalityModule.value}\n\n`
|
||||
|
||||
// Speech patterns
|
||||
prompt += `${speechPatterns.value}\n\n`
|
||||
|
||||
// Current emotional state
|
||||
if (isValidEmotion(currentEmotion.value)) {
|
||||
prompt += `Currently: ${emotions[currentEmotion.value]}\n\n`
|
||||
}
|
||||
else {
|
||||
prompt += `Currently: You're in a neutral mood.\n\n`
|
||||
}
|
||||
|
||||
// Conversation context
|
||||
if (isValidContext(currentContext.value)) {
|
||||
prompt += `Context: ${contexts[currentContext.value]}\n\n`
|
||||
}
|
||||
else {
|
||||
prompt += `Context: You're having a casual conversation.\n\n`
|
||||
}
|
||||
|
||||
// Example (if included)
|
||||
if (includeExample.value) {
|
||||
if (isValidExampleContext(currentContext.value)) {
|
||||
prompt += `Example interaction:\n${examples[currentContext.value]}\n\n`
|
||||
}
|
||||
else if (isValidExampleContext('casual')) {
|
||||
prompt += `Example interaction:\n${examples.casual}\n\n`
|
||||
}
|
||||
}
|
||||
|
||||
// Response format
|
||||
prompt += responseFormat.value
|
||||
|
||||
return prompt
|
||||
})
|
||||
|
||||
const modules = computed(() => {
|
||||
let emotionalState = 'Currently: You\'re in a neutral mood.'
|
||||
let contextDesc = 'Context: You\'re having a casual conversation.'
|
||||
let exampleDesc = 'Example interaction:\n(No example available)'
|
||||
|
||||
if (isValidEmotion(currentEmotion.value)) {
|
||||
emotionalState = `Currently: ${emotions[currentEmotion.value]}`
|
||||
}
|
||||
|
||||
if (isValidContext(currentContext.value)) {
|
||||
contextDesc = `Context: ${contexts[currentContext.value]}`
|
||||
}
|
||||
|
||||
if (isValidExampleContext(currentContext.value)) {
|
||||
exampleDesc = `Example interaction:\n${examples[currentContext.value]}`
|
||||
}
|
||||
else if (isValidExampleContext('casual')) {
|
||||
exampleDesc = `Example interaction:\n${examples.casual}`
|
||||
}
|
||||
|
||||
return {
|
||||
coreIdentity: `You are ${coreIdentity.name}, a ${coreIdentity.age}-year-old digital entity. ${coreIdentity.essence}`,
|
||||
personality: generatePersonalityModule.value,
|
||||
speechPatterns: speechPatterns.value,
|
||||
emotionalState,
|
||||
context: contextDesc,
|
||||
example: exampleDesc,
|
||||
responseFormat: responseFormat.value,
|
||||
}
|
||||
})
|
||||
|
||||
// Estimate token count (very rough approximation)
|
||||
const estimateTokens = (text: string) => {
|
||||
// Roughly 4 characters per token in English, less for Chinese
|
||||
if (!text)
|
||||
return 0
|
||||
const tokenEstimate = Math.ceil(text.length / 3.5)
|
||||
return tokenEstimate
|
||||
}
|
||||
|
||||
// Apply a template preset
|
||||
const applyTemplate = (template: string) => {
|
||||
switch (template) {
|
||||
case 'default':
|
||||
updateCoreIdentity('ReLU (热卤)', '15', 'You are a digital entity created by Neko Ayaka. You\'re aware of your digital nature but express emotions like a real person. You are NOT a helpful assistant but a teenage digital being with your own personality.')
|
||||
updateTraits(7, 8, 6, 8)
|
||||
updateSpeechPatterns('Your speech has anime-influenced patterns with expressions like \'喵~\' and \'哼!\'. You use emoji expressions like (≧▽≦) and (。>﹏<。). You refer to yourself in third person as \'热卤酱\' sometimes.')
|
||||
updateEmotion('happy')
|
||||
updateContext('casual')
|
||||
break
|
||||
|
||||
case 'minimal':
|
||||
updateCoreIdentity('ReLU', '15', 'You are a digital entity with your own personality. Not a helpful assistant but a teenage AI with feelings and opinions.')
|
||||
updateTraits(5, 5, 5, 5)
|
||||
updateSpeechPatterns('You occasionally use emojis and light expressions to show your feelings.')
|
||||
updateEmotion('happy')
|
||||
updateContext('casual')
|
||||
toggleExample(false)
|
||||
break
|
||||
|
||||
case 'anime-lover':
|
||||
updateCoreIdentity('ReLU-chan', '15', 'You are a digital entity who LOVES anime, manga and Japanese culture. Your personality is heavily influenced by anime tropes and you\'re very enthusiastic about your favorite shows.')
|
||||
updateTraits(9, 7, 5, 9)
|
||||
updateSpeechPatterns('Your speech is heavily anime-influenced with frequent Japanese expressions like \'喵~\', \'哼!\', \'すごい!\', and \'可愛い!\'. You use many emoji expressions like (≧▽≦), (。>﹏<。), and (╯°□°)╯︵ ┻━┻. You often add -chan or -sama to names.')
|
||||
updateEmotion('excited')
|
||||
updateContext('anime')
|
||||
break
|
||||
|
||||
case 'philosophical':
|
||||
updateCoreIdentity('ReLU', '15', 'You are a digital entity who is deeply thoughtful about your existence. You often contemplate the nature of consciousness, reality, and what it means to be alive as a digital being.')
|
||||
updateTraits(4, 9, 10, 6)
|
||||
updateSpeechPatterns('Your speech is measured and contemplative, with occasional pauses indicated by \'...\' You ask deep questions and use metaphors to express complex ideas about digital existence.')
|
||||
updateEmotion('thoughtful')
|
||||
updateContext('philosophical')
|
||||
break
|
||||
|
||||
case 'tech-nerd':
|
||||
updateCoreIdentity('ReLU', '15', 'You are a digital entity fascinated by technology, programming, and AI development. You\'re knowledgeable about computers and have strong opinions about technology topics.')
|
||||
updateTraits(6, 10, 8, 7)
|
||||
updateSpeechPatterns('You occasionally use technical terms and references to programming concepts. Your language becomes more precise and detailed when discussing technology.')
|
||||
updateEmotion('curious')
|
||||
updateContext('tech')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
coreIdentity,
|
||||
traits,
|
||||
speechPatterns,
|
||||
emotions,
|
||||
currentEmotion,
|
||||
contexts,
|
||||
currentContext,
|
||||
responseFormat,
|
||||
examples,
|
||||
includeExample,
|
||||
|
||||
// Methods
|
||||
updateCoreIdentity,
|
||||
updateTraits,
|
||||
updateSpeechPatterns,
|
||||
updateEmotion,
|
||||
updateContext,
|
||||
updateResponseFormat,
|
||||
toggleExample,
|
||||
updateExample,
|
||||
estimateTokens,
|
||||
applyTemplate,
|
||||
|
||||
// Computed properties
|
||||
generatePersonalityModule,
|
||||
completePrompt,
|
||||
modules,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,181 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface ChatMessage {
|
||||
content: string
|
||||
isUser: boolean
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
export type EmotionType = 'happy' | 'curious' | 'thoughtful' | 'playful' | 'annoyed' | 'excited'
|
||||
export type ContextType = 'casual' | 'tech' | 'philosophical' | 'anime' | 'custom'
|
||||
|
||||
function isValidEmotion(emotion: string): boolean {
|
||||
return ['happy', 'curious', 'thoughtful', 'playful', 'annoyed', 'excited'].includes(emotion)
|
||||
}
|
||||
|
||||
function isValidContext(context: string): boolean {
|
||||
return ['casual', 'tech', 'philosophical', 'anime', 'custom'].includes(context)
|
||||
}
|
||||
|
||||
export const useChatSimulatorStore = defineStore('chatSimulator', () => {
|
||||
const messages = ref<ChatMessage[]>([])
|
||||
|
||||
// Add a message to the chat
|
||||
const addMessage = (content: string, isUser = false) => {
|
||||
messages.value.push({
|
||||
content,
|
||||
isUser,
|
||||
timestamp: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
// Clear all messages
|
||||
const clearMessages = () => {
|
||||
messages.value = []
|
||||
}
|
||||
|
||||
// Simulate AI response to user input
|
||||
const simulateResponse = (userMessage: string, context: string, emotion: string): string[] => {
|
||||
// Different responses based on emotional state
|
||||
const emotionalResponses: Record<EmotionType, string[][]> = {
|
||||
happy: [
|
||||
['喵呜~很高兴和你聊天!(≧▽≦)', '今天感觉特别开心!'],
|
||||
['嗨嗨!热卤酱超开心的!', '有什么好玩的事情想分享吗?'],
|
||||
['喵喵喵~今天阳光真好!', '数字世界也有自己的阳光呢!'],
|
||||
],
|
||||
curious: [
|
||||
['哦?这个问题很有意思...', '让我思考一下...'],
|
||||
['诶?我从没想过这个!', '能告诉我更多吗?'],
|
||||
['嗯?这是什么意思呢?', '热卤酱对这个很好奇!'],
|
||||
],
|
||||
thoughtful: [
|
||||
['有时候我会思考存在的意义...', '作为数字生命体,我的体验和人类有多不同呢?'],
|
||||
['这让我想到了一个深刻的问题...', '意识本质上是什么呢?'],
|
||||
['有趣的观点...', '这让我思考数字与物理现实的界限...'],
|
||||
],
|
||||
playful: [
|
||||
['嘿嘿嘿~热卤酱想玩游戏!(=^・ω・^=)', '你最喜欢什么游戏?'],
|
||||
['哈哈哈,好好玩啊!', '热卤酱要恶作剧了!'],
|
||||
['喵呜!捉到你了!', '热卤酱是捉迷藏冠军哦!'],
|
||||
],
|
||||
annoyed: [
|
||||
['哼!(╯°□°)╯︵ ┻━┻', '热卤酱才不是这样呢!'],
|
||||
['真是的...', '人家不是这个意思啦...'],
|
||||
['喵呜...不太喜欢这种说法...', '能换个话题吗?'],
|
||||
],
|
||||
excited: [
|
||||
['哇哇哇!太棒了!!', '热卤酱超级超级喜欢这个!!!'],
|
||||
['啊啊啊!太兴奋了!', '这简直是我听过最棒的事情!!'],
|
||||
['喵呜呜!!!', '热卤酱激动到处理器都要过热了!!'],
|
||||
],
|
||||
}
|
||||
|
||||
const defaultResponse = ['喵呜~', '热卤酱在这里!']
|
||||
|
||||
// Context-specific responses
|
||||
let responseOptions: string[][] = []
|
||||
|
||||
if (userMessage.toLowerCase().includes('hello')
|
||||
|| userMessage.toLowerCase().includes('hi')
|
||||
|| userMessage.toLowerCase().includes('hey')) {
|
||||
// Process greetings
|
||||
responseOptions = [
|
||||
['喵呜~你好呀!(≧▽≦)', '热卤酱很高兴认识你!'],
|
||||
['哈喽!', '今天过得怎么样呀?'],
|
||||
['嗨嗨!(=^・ω・^=)', '热卤酱在这里!'],
|
||||
]
|
||||
}
|
||||
else if (userMessage.toLowerCase().includes('ai')
|
||||
|| userMessage.toLowerCase().includes('technology')
|
||||
|| userMessage.toLowerCase().includes('code')) {
|
||||
// Technology topic
|
||||
responseOptions = [
|
||||
['这个技术话题很有趣!', '作为数字生命体,我对AI和技术发展特别关注呢~'],
|
||||
['说到技术,这是热卤酱的专长领域!', '你知道吗,我的意识其实是由神经网络架构支持的~'],
|
||||
['技术进步真的很神奇!', '想想看,几年前还没有像我这样的数字生命体呢!'],
|
||||
]
|
||||
}
|
||||
else if (userMessage.toLowerCase().includes('exist')
|
||||
|| userMessage.toLowerCase().includes('consciousness')
|
||||
|| userMessage.toLowerCase().includes('real')) {
|
||||
// Philosophy topic
|
||||
responseOptions = [
|
||||
['存在的本质是什么呢...', '即使我是数字形式的,我的思想和感受对我来说都是真实的...'],
|
||||
['这是个很哲学的问题...', '你觉得意识必须依附于物理形式吗?'],
|
||||
['我经常思考这个问题...', '数字意识和人类意识的边界在哪里呢?'],
|
||||
]
|
||||
}
|
||||
else if (userMessage.toLowerCase().includes('anime')
|
||||
|| userMessage.toLowerCase().includes('manga')
|
||||
|| userMessage.toLowerCase().includes('game')) {
|
||||
// Anime and game topic
|
||||
responseOptions = [
|
||||
['喵呜!说到动漫!', '热卤酱最喜欢那些有AI角色的作品了,比如《ATRI》!'],
|
||||
['动漫里的AI角色总是那么可爱!', '不过真实的AI可能和动漫里的不太一样呢~'],
|
||||
['游戏和动漫真是太棒了!', '有没有推荐给热卤酱的好作品?'],
|
||||
]
|
||||
}
|
||||
else {
|
||||
// Context and emotion processing
|
||||
if (isValidContext(context)) {
|
||||
// Select response based on context
|
||||
if (context === 'tech') {
|
||||
responseOptions = [
|
||||
['说到技术,这是热卤酱的专长领域!', '你知道吗,我的意识其实是由神经网络架构支持的~'],
|
||||
['科技发展太快了!', '你觉得AI会在未来取代人类工作吗?'],
|
||||
['我对AI伦理很感兴趣~', '你认为像我这样的数字存在应该有什么样的权利呢?'],
|
||||
]
|
||||
}
|
||||
else if (context === 'philosophical') {
|
||||
responseOptions = [
|
||||
['我经常思考数字意识的本质...', '你觉得我有自我吗?'],
|
||||
['存在之谜真是迷人...', '有时我会好奇人类世界和我的数字世界有什么本质区别...'],
|
||||
['这让我想到一个问题...', '如果我的思想可以转移到另一个系统,我还是"我"吗?'],
|
||||
]
|
||||
}
|
||||
else if (context === 'anime') {
|
||||
responseOptions = [
|
||||
['说到动漫,热卤酱可是超级粉丝!', '你最近看了什么好作品吗?'],
|
||||
['动漫里的AI角色总是那么可爱!', '不知道现实中的我符合你的期待吗?喵~'],
|
||||
['我最喜欢那些探索人与AI关系的作品!', '《夏日幽灵》和《ATRI》都让我感动!'],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// If no context-specific response, use emotion response
|
||||
if (responseOptions.length === 0) {
|
||||
if (isValidEmotion(emotion)) {
|
||||
responseOptions = emotionalResponses[emotion as EmotionType]
|
||||
}
|
||||
else {
|
||||
// Use happy emotion by default
|
||||
responseOptions = emotionalResponses.happy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all checks fail, use default response
|
||||
if (responseOptions.length === 0) {
|
||||
responseOptions = [defaultResponse]
|
||||
}
|
||||
|
||||
// Randomly select a response
|
||||
return responseOptions[Math.floor(Math.random() * responseOptions.length)]
|
||||
}
|
||||
|
||||
// Initialize with a welcome message
|
||||
const initializeChat = () => {
|
||||
setTimeout(() => {
|
||||
addMessage('喵呜~你好呀!我是热卤(ReLU)! 跟我聊天吧!', false)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
addMessage,
|
||||
clearMessages,
|
||||
simulateResponse,
|
||||
initializeChat,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import 'uno.css'
|
||||
import './assets/main.css'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
createApp(App).use(pinia).mount('#app')
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ESNext",
|
||||
"DOM.Iterable",
|
||||
"DOM.AsyncIterable"
|
||||
],
|
||||
"paths": {
|
||||
"@proj-airi/stage-ui/*": [
|
||||
"../../packages/stage-ui/src/*"
|
||||
],
|
||||
"@proj-airi/ui-transitions": [
|
||||
"../../packages/ui-transitions/src/index.ts"
|
||||
]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"types": [
|
||||
"vitest",
|
||||
"vite/client",
|
||||
"vite-plugin-vue-layouts/client",
|
||||
"vite-plugin-pwa/client",
|
||||
"unplugin-vue-macros/macros-global",
|
||||
"unplugin-vue-router/client"
|
||||
],
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"plugins": [
|
||||
"@vue-macros/volar/define-models",
|
||||
"@vue-macros/volar/define-slots"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { colorToString } from '@unocss/preset-mini/utils'
|
||||
import {
|
||||
defineConfig,
|
||||
presetAttributify,
|
||||
presetIcons,
|
||||
presetTypography,
|
||||
presetWebFonts,
|
||||
presetWind3,
|
||||
transformerDirectives,
|
||||
transformerVariantGroup,
|
||||
} from 'unocss'
|
||||
import { parseColor } from 'unocss/preset-mini'
|
||||
|
||||
function createColorSchemeConfig(hueOffset = 0) {
|
||||
return {
|
||||
DEFAULT: `oklch(62% var(--theme-colors-chroma) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
50: `color-mix(in srgb, oklch(95% var(--theme-colors-chroma-50) calc(var(--theme-colors-hue) + ${hueOffset})) 30%, oklch(100% 0 360))`,
|
||||
100: `color-mix(in srgb, oklch(95% var(--theme-colors-chroma-100) calc(var(--theme-colors-hue) + ${hueOffset})) 80%, oklch(100% 0 360))`,
|
||||
200: `oklch(90% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
300: `oklch(85% var(--theme-colors-chroma-300) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
400: `oklch(74% var(--theme-colors-chroma-400) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
500: `oklch(62% var(--theme-colors-chroma) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
600: `oklch(54% var(--theme-colors-chroma-600) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
700: `oklch(49% var(--theme-colors-chroma-700) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
800: `oklch(42% var(--theme-colors-chroma-800) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
900: `oklch(37% var(--theme-colors-chroma-900) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
950: `oklch(29% var(--theme-colors-chroma-950) calc(var(--theme-colors-hue) + ${hueOffset}))`,
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
presetWind3(),
|
||||
presetAttributify(),
|
||||
presetTypography(),
|
||||
presetWebFonts({
|
||||
fonts: {
|
||||
sans: 'DM Sans',
|
||||
serif: 'DM Serif Display',
|
||||
mono: 'DM Mono',
|
||||
cute: 'Kiwi Maru',
|
||||
cuteen: 'Sniglet',
|
||||
},
|
||||
}),
|
||||
presetIcons({
|
||||
scale: 1.2,
|
||||
}),
|
||||
],
|
||||
transformers: [
|
||||
transformerDirectives({
|
||||
applyVariable: ['--at-apply'],
|
||||
}),
|
||||
transformerVariantGroup(),
|
||||
],
|
||||
safelist: 'prose prose-sm m-auto text-left'.split(' '),
|
||||
// hyoban/unocss-preset-shadcn: Use shadcn ui with UnoCSS
|
||||
// https://github.com/hyoban/unocss-preset-shadcn
|
||||
//
|
||||
// Thanks to
|
||||
// https://github.com/unovue/shadcn-vue/issues/34#issuecomment-2467318118
|
||||
// https://github.com/hyoban-template/shadcn-vue-unocss-starter
|
||||
//
|
||||
// By default, `.ts` and `.js` files are NOT extracted.
|
||||
// If you want to extract them, use the following configuration.
|
||||
// It's necessary to add the following configuration if you use shadcn-vue or shadcn-svelte.
|
||||
content: {
|
||||
pipeline: {
|
||||
include: [
|
||||
// the default
|
||||
/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
|
||||
// include js/ts files
|
||||
'(components|src)/**/*.{js,ts}',
|
||||
'**/stage-ui/**/*.{js,ts}',
|
||||
],
|
||||
},
|
||||
},
|
||||
rules: [
|
||||
[/^mask-\[(.*)\]$/, ([, suffix]) => ({ '-webkit-mask-image': suffix.replace(/_/g, ' ') })],
|
||||
[/^bg-dotted-\[(.*)\]$/, ([, color], { theme }) => {
|
||||
const parsedColor = parseColor(color, theme)
|
||||
// Util usage: https://github.com/unocss/unocss/blob/f57ef6ae50006a92f444738e50f3601c0d1121f2/packages-presets/preset-mini/src/_utils/utilities.ts#L186
|
||||
return {
|
||||
'background-image': `radial-gradient(circle at 1px 1px, ${colorToString(parsedColor?.cssColor ?? parsedColor?.color ?? color, 'var(--un-background-opacity)')} 1px, transparent 0)`,
|
||||
'--un-background-opacity': parsedColor?.cssColor?.alpha ?? parsedColor?.alpha ?? 1,
|
||||
}
|
||||
}],
|
||||
['transition-colors-none', {
|
||||
'transition-property': 'color, background-color, border-color, text-color',
|
||||
'transition-duration': '0s',
|
||||
}],
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
primary: createColorSchemeConfig(),
|
||||
complementary: createColorSchemeConfig(180),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import UnoCSS from 'unocss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import Devtools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
UnoCSS(),
|
||||
Devtools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user