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:
RainbowBird
2025-04-09 11:11:22 +08:00
committed by GitHub
co-authored by Neko Ayaka
parent 63205e1344
commit c43bbb0edb
16 changed files with 1835 additions and 19 deletions
@@ -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)),
},
},
})