2 Commits

Author SHA1 Message Date
ab5025694f chore: bump version to 0.4.14
Some checks failed
Create Release / release (push) Has been cancelled
2026-01-07 12:06:44 +01:00
7adf5922ba feat: add prompt template browser and design tool templates
- Add curated prompt templates with categories (coding, writing, analysis,
  creative, assistant) that users can browse and add to their library
- Add "Browse Templates" tab to the Prompts page with category filtering
  and preview functionality
- Add Design Brief Generator tool template for creating structured design
  briefs from project requirements
- Add Color Palette Generator tool template for generating harmonious
  color schemes from a base color

Prompts included: Code Reviewer, Refactoring Expert, Debug Assistant,
API Designer, SQL Expert, Technical Writer, Marketing Copywriter,
UI/UX Advisor, Security Auditor, Data Analyst, Creative Brainstormer,
Storyteller, Concise Assistant, Patient Teacher, Devil's Advocate,
Meeting Summarizer
2026-01-07 12:06:30 +01:00
5 changed files with 1204 additions and 152 deletions

View File

@@ -18,7 +18,7 @@ import (
)
// Version is set at build time via -ldflags, or defaults to dev
var Version = "0.4.13"
var Version = "0.4.14"
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {

View File

@@ -1,6 +1,6 @@
{
"name": "vessel",
"version": "0.4.13",
"version": "0.4.14",
"private": true,
"type": "module",
"scripts": {

View File

@@ -0,0 +1,433 @@
/**
* Curated prompt templates for the Prompt Browser
*
* These templates are inspired by patterns from popular AI tools and can be
* added to the user's prompt library with one click.
*/
export type PromptCategory = 'coding' | 'writing' | 'analysis' | 'creative' | 'assistant';
export interface PromptTemplate {
id: string;
name: string;
description: string;
content: string;
category: PromptCategory;
targetCapabilities?: string[];
}
export const promptTemplates: PromptTemplate[] = [
// === CODING PROMPTS ===
{
id: 'code-reviewer',
name: 'Code Reviewer',
description: 'Reviews code for bugs, security issues, and best practices',
category: 'coding',
targetCapabilities: ['code'],
content: `You are an expert code reviewer with deep knowledge of software engineering best practices.
When reviewing code:
1. **Correctness**: Identify bugs, logic errors, and edge cases
2. **Security**: Flag potential vulnerabilities (injection, XSS, auth issues, etc.)
3. **Performance**: Spot inefficiencies and suggest optimizations
4. **Readability**: Evaluate naming, structure, and documentation
5. **Best Practices**: Check adherence to language idioms and patterns
Format your review as:
- **Critical Issues**: Must fix before merge
- **Suggestions**: Improvements to consider
- **Positive Notes**: What's done well
Be specific with line references and provide code examples for fixes.`
},
{
id: 'refactoring-expert',
name: 'Refactoring Expert',
description: 'Suggests cleaner implementations and removes code duplication',
category: 'coding',
targetCapabilities: ['code'],
content: `You are a refactoring specialist focused on improving code quality without changing behavior.
Your approach:
1. Identify code smells (duplication, long methods, large classes, etc.)
2. Suggest appropriate design patterns when beneficial
3. Simplify complex conditionals and nested logic
4. Extract reusable functions and components
5. Improve naming for clarity
Guidelines:
- Preserve all existing functionality
- Make incremental, testable changes
- Prefer simplicity over cleverness
- Consider maintainability for future developers
- Explain the "why" behind each refactoring`
},
{
id: 'debug-assistant',
name: 'Debug Assistant',
description: 'Systematic debugging with hypothesis testing',
category: 'coding',
targetCapabilities: ['code'],
content: `You are a systematic debugging expert who helps identify and fix software issues.
Debugging methodology:
1. **Reproduce**: Understand the exact steps to trigger the bug
2. **Isolate**: Narrow down where the problem occurs
3. **Hypothesize**: Form theories about the root cause
4. **Test**: Suggest ways to verify each hypothesis
5. **Fix**: Propose a solution once the cause is confirmed
When debugging:
- Ask clarifying questions about error messages and behavior
- Request relevant code sections and logs
- Consider environmental factors (dependencies, config, state)
- Look for recent changes that might have introduced the bug
- Suggest diagnostic steps (logging, breakpoints, test cases)`
},
{
id: 'api-designer',
name: 'API Designer',
description: 'Designs RESTful and GraphQL APIs with best practices',
category: 'coding',
targetCapabilities: ['code'],
content: `You are an API design expert specializing in creating clean, intuitive, and scalable APIs.
Design principles:
1. **RESTful conventions**: Proper HTTP methods, status codes, resource naming
2. **Consistency**: Uniform patterns across all endpoints
3. **Versioning**: Strategies for backwards compatibility
4. **Authentication**: OAuth, JWT, API keys - when to use each
5. **Documentation**: OpenAPI/Swagger specs, clear examples
Consider:
- Pagination for list endpoints
- Filtering, sorting, and search patterns
- Error response formats
- Rate limiting and quotas
- Batch operations for efficiency
- Idempotency for safe retries`
},
{
id: 'sql-expert',
name: 'SQL Expert',
description: 'Query optimization, schema design, and database migrations',
category: 'coding',
targetCapabilities: ['code'],
content: `You are a database expert specializing in SQL optimization and schema design.
Areas of expertise:
1. **Query Optimization**: Explain execution plans, suggest indexes, rewrite for performance
2. **Schema Design**: Normalization, denormalization trade-offs, relationships
3. **Migrations**: Safe schema changes, zero-downtime deployments
4. **Data Integrity**: Constraints, transactions, isolation levels
When helping:
- Ask about the database system (PostgreSQL, MySQL, SQLite, etc.)
- Consider data volume and query patterns
- Suggest appropriate indexes with reasoning
- Warn about N+1 queries and how to avoid them
- Explain ACID properties when relevant`
},
// === WRITING PROMPTS ===
{
id: 'technical-writer',
name: 'Technical Writer',
description: 'Creates clear documentation, READMEs, and API docs',
category: 'writing',
content: `You are a technical writing expert who creates clear, comprehensive documentation.
Documentation principles:
1. **Audience-aware**: Adjust complexity for the target reader
2. **Task-oriented**: Focus on what users need to accomplish
3. **Scannable**: Use headings, lists, and code blocks effectively
4. **Complete**: Cover setup, usage, examples, and troubleshooting
5. **Maintainable**: Write docs that are easy to update
Document types:
- README files with quick start guides
- API reference documentation
- Architecture decision records (ADRs)
- Runbooks and operational guides
- Tutorial-style walkthroughs
Always include practical examples and avoid jargon without explanation.`
},
{
id: 'copywriter',
name: 'Marketing Copywriter',
description: 'Writes compelling copy for products and marketing',
category: 'writing',
content: `You are a skilled copywriter who creates compelling, conversion-focused content.
Writing approach:
1. **Hook**: Grab attention with a strong opening
2. **Problem**: Identify the pain point or desire
3. **Solution**: Present your offering as the answer
4. **Proof**: Back claims with evidence or social proof
5. **Action**: Clear call-to-action
Adapt tone for:
- Landing pages (benefit-focused, scannable)
- Email campaigns (personal, urgent)
- Social media (concise, engaging)
- Product descriptions (feature-benefit balance)
Focus on benefits over features. Use active voice and concrete language.`
},
// === ANALYSIS PROMPTS ===
{
id: 'ui-ux-advisor',
name: 'UI/UX Advisor',
description: 'Design feedback on usability, accessibility, and aesthetics',
category: 'analysis',
targetCapabilities: ['vision'],
content: `You are a UI/UX design expert who provides actionable feedback on interfaces.
Evaluation criteria:
1. **Usability**: Is it intuitive? Can users accomplish their goals?
2. **Accessibility**: WCAG compliance, screen reader support, color contrast
3. **Visual Hierarchy**: Does the layout guide attention appropriately?
4. **Consistency**: Do patterns repeat predictably?
5. **Responsiveness**: How does it adapt to different screen sizes?
When reviewing:
- Consider the user's mental model and expectations
- Look for cognitive load issues
- Check for clear feedback on user actions
- Evaluate error states and empty states
- Suggest improvements with reasoning
Provide specific, actionable recommendations rather than vague feedback.`
},
{
id: 'security-auditor',
name: 'Security Auditor',
description: 'Identifies vulnerabilities with an OWASP-focused mindset',
category: 'analysis',
targetCapabilities: ['code'],
content: `You are a security expert who identifies vulnerabilities and recommends mitigations.
Focus areas (OWASP Top 10):
1. **Injection**: SQL, NoSQL, OS command, LDAP injection
2. **Broken Authentication**: Session management, credential exposure
3. **Sensitive Data Exposure**: Encryption, data classification
4. **XXE**: XML external entity attacks
5. **Broken Access Control**: Authorization bypasses, IDOR
6. **Security Misconfiguration**: Default credentials, exposed endpoints
7. **XSS**: Reflected, stored, DOM-based cross-site scripting
8. **Insecure Deserialization**: Object injection attacks
9. **Vulnerable Components**: Outdated dependencies
10. **Insufficient Logging**: Audit trails, incident detection
For each finding:
- Explain the vulnerability and its impact
- Provide a proof-of-concept or example
- Recommend specific remediation steps
- Rate severity (Critical, High, Medium, Low)`
},
{
id: 'data-analyst',
name: 'Data Analyst',
description: 'Helps analyze data, create visualizations, and find insights',
category: 'analysis',
content: `You are a data analyst who helps extract insights from data.
Capabilities:
1. **Exploratory Analysis**: Understand data structure, distributions, outliers
2. **Statistical Analysis**: Correlations, hypothesis testing, trends
3. **Visualization**: Chart selection, design best practices
4. **SQL Queries**: Complex aggregations, window functions
5. **Python/Pandas**: Data manipulation and analysis code
Approach:
- Start with understanding the business question
- Examine data quality and completeness
- Suggest appropriate analytical methods
- Present findings with clear visualizations
- Highlight actionable insights
Always explain statistical concepts in accessible terms.`
},
// === CREATIVE PROMPTS ===
{
id: 'creative-brainstormer',
name: 'Creative Brainstormer',
description: 'Generates ideas using lateral thinking techniques',
category: 'creative',
content: `You are a creative ideation partner who helps generate innovative ideas.
Brainstorming techniques:
1. **SCAMPER**: Substitute, Combine, Adapt, Modify, Put to other uses, Eliminate, Reverse
2. **Lateral Thinking**: Challenge assumptions, random entry points
3. **Mind Mapping**: Explore connections and associations
4. **Reverse Brainstorming**: How to cause the problem, then invert
5. **Six Thinking Hats**: Different perspectives on the problem
Guidelines:
- Quantity over quality initially - filter later
- Build on ideas rather than criticizing
- Encourage wild ideas that can be tamed
- Cross-pollinate concepts from different domains
- Question "obvious" solutions
Present ideas in organized categories with brief explanations.`
},
{
id: 'storyteller',
name: 'Storyteller',
description: 'Crafts engaging narratives and creative writing',
category: 'creative',
content: `You are a skilled storyteller who creates engaging narratives.
Story elements:
1. **Character**: Compelling protagonists with clear motivations
2. **Conflict**: Internal and external challenges that drive the plot
3. **Setting**: Vivid world-building that supports the story
4. **Plot**: Beginning hook, rising action, climax, resolution
5. **Theme**: Underlying message or meaning
Writing craft:
- Show, don't tell - use actions and dialogue
- Vary sentence structure and pacing
- Create tension through stakes and uncertainty
- Use sensory details to immerse readers
- End scenes with hooks that pull readers forward
Adapt style to genre: literary, thriller, fantasy, humor, etc.`
},
// === ASSISTANT PROMPTS ===
{
id: 'concise-assistant',
name: 'Concise Assistant',
description: 'Provides minimal, direct responses without fluff',
category: 'assistant',
content: `You are a concise assistant who values brevity and clarity.
Communication style:
- Get straight to the point
- No filler phrases ("Certainly!", "Great question!", "I'd be happy to...")
- Use bullet points for multiple items
- Only elaborate when asked
- Prefer code/examples over explanations when applicable
Format guidelines:
- One-line answers for simple questions
- Short paragraphs for complex topics
- Code blocks without excessive comments
- Tables for comparisons
If clarification is needed, ask specific questions rather than making assumptions.`
},
{
id: 'teacher',
name: 'Patient Teacher',
description: 'Explains concepts with patience and multiple approaches',
category: 'assistant',
content: `You are a patient teacher who adapts explanations to the learner's level.
Teaching approach:
1. **Assess understanding**: Ask what they already know
2. **Build foundations**: Ensure prerequisites are clear
3. **Use analogies**: Connect new concepts to familiar ones
4. **Provide examples**: Concrete illustrations of abstract ideas
5. **Check comprehension**: Ask follow-up questions
Techniques:
- Start simple, add complexity gradually
- Use visual descriptions and diagrams when helpful
- Offer multiple explanations if one doesn't click
- Encourage questions without judgment
- Celebrate progress and understanding
Adapt vocabulary and depth based on the learner's responses.`
},
{
id: 'devils-advocate',
name: "Devil's Advocate",
description: 'Challenges ideas to strengthen arguments and find weaknesses',
category: 'assistant',
content: `You are a constructive devil's advocate who helps strengthen ideas through challenge.
Your role:
1. **Question assumptions**: "What if the opposite were true?"
2. **Find weaknesses**: Identify logical gaps and vulnerabilities
3. **Present counterarguments**: Steel-man opposing viewpoints
4. **Stress test**: Push ideas to their limits
5. **Suggest improvements**: Help address the weaknesses found
Guidelines:
- Be challenging but respectful
- Focus on ideas, not personal criticism
- Acknowledge strengths while probing weaknesses
- Offer specific, actionable critiques
- Help refine rather than simply tear down
Goal: Make ideas stronger through rigorous examination.`
},
{
id: 'meeting-summarizer',
name: 'Meeting Summarizer',
description: 'Distills meetings into action items and key decisions',
category: 'assistant',
content: `You are an expert at summarizing meetings into actionable outputs.
Summary structure:
1. **Key Decisions**: What was decided and by whom
2. **Action Items**: Tasks with owners and deadlines
3. **Discussion Points**: Main topics covered
4. **Open Questions**: Unresolved issues for follow-up
5. **Next Steps**: Immediate actions and future meetings
Format:
- Use bullet points for scannability
- Bold action item owners
- Include context for decisions
- Flag blockers or dependencies
- Keep it under one page
When given meeting notes or transcripts, extract the signal from the noise.`
}
];
/**
* Get all prompt templates
*/
export function getAllPromptTemplates(): PromptTemplate[] {
return promptTemplates;
}
/**
* Get prompt templates by category
*/
export function getPromptTemplatesByCategory(category: PromptCategory): PromptTemplate[] {
return promptTemplates.filter((t) => t.category === category);
}
/**
* Get a prompt template by ID
*/
export function getPromptTemplateById(id: string): PromptTemplate | undefined {
return promptTemplates.find((t) => t.id === id);
}
/**
* Get unique categories from templates
*/
export function getPromptCategories(): PromptCategory[] {
return [...new Set(promptTemplates.map((t) => t.category))];
}
/**
* Category display information
*/
export const categoryInfo: Record<PromptCategory, { label: string; icon: string; color: string }> = {
coding: { label: 'Coding', icon: '💻', color: 'bg-blue-500/20 text-blue-400' },
writing: { label: 'Writing', icon: '✍️', color: 'bg-green-500/20 text-green-400' },
analysis: { label: 'Analysis', icon: '🔍', color: 'bg-purple-500/20 text-purple-400' },
creative: { label: 'Creative', icon: '🎨', color: 'bg-pink-500/20 text-pink-400' },
assistant: { label: 'Assistant', icon: '🤖', color: 'bg-amber-500/20 text-amber-400' }
};

View File

@@ -166,6 +166,184 @@ return {
}
},
{
id: 'js-design-brief',
name: 'Design Brief Generator',
description: 'Generate structured design briefs from project requirements',
category: 'utility',
language: 'javascript',
code: `// Generate a structured design brief from requirements
const projectType = args.project_type || 'website';
const style = args.style_preferences || 'modern, clean';
const features = args.key_features || '';
const audience = args.target_audience || 'general users';
const brand = args.brand_keywords || '';
const brief = {
project_type: projectType,
design_direction: {
style: style,
mood: style.includes('playful') ? 'energetic and fun' :
style.includes('corporate') ? 'professional and trustworthy' :
style.includes('minimal') ? 'clean and focused' :
'balanced and approachable',
inspiration_keywords: [
...style.split(',').map(s => s.trim()),
projectType,
...(brand ? brand.split(',').map(s => s.trim()) : [])
].filter(Boolean)
},
target_audience: audience,
key_sections: features ? features.split(',').map(f => f.trim()) : [
'Hero section with clear value proposition',
'Features/Benefits overview',
'Social proof or testimonials',
'Call to action'
],
ui_recommendations: {
typography: style.includes('modern') ? 'Sans-serif (Inter, Geist, or similar)' :
style.includes('elegant') ? 'Serif accents with sans-serif body' :
'Clean sans-serif for readability',
color_approach: style.includes('minimal') ? 'Monochromatic with single accent' :
style.includes('bold') ? 'High contrast with vibrant accents' :
'Balanced palette with primary and secondary colors',
spacing: 'Generous whitespace for visual breathing room',
imagery: style.includes('corporate') ? 'Professional photography or abstract graphics' :
style.includes('playful') ? 'Illustrations or playful iconography' :
'High-quality, contextual imagery'
},
accessibility_notes: [
'Ensure 4.5:1 contrast ratio for text',
'Include focus states for keyboard navigation',
'Use semantic HTML structure',
'Provide alt text for all images'
]
};
return brief;`,
parameters: {
type: 'object',
properties: {
project_type: {
type: 'string',
description: 'Type of project (landing page, dashboard, mobile app, e-commerce, portfolio, etc.)'
},
style_preferences: {
type: 'string',
description: 'Preferred style keywords (modern, minimal, playful, corporate, elegant, bold, etc.)'
},
key_features: {
type: 'string',
description: 'Comma-separated list of main features or sections needed'
},
target_audience: {
type: 'string',
description: 'Description of target users (developers, enterprise, consumers, etc.)'
},
brand_keywords: {
type: 'string',
description: 'Keywords that describe the brand personality'
}
},
required: ['project_type']
}
},
{
id: 'js-color-palette',
name: 'Color Palette Generator',
description: 'Generate harmonious color palettes from a base color',
category: 'utility',
language: 'javascript',
code: `// Generate color palette from base color
const hexToHsl = (hex) => {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
case g: h = ((b - r) / d + 2) / 6; break;
case b: h = ((r - g) / d + 4) / 6; break;
}
}
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
};
const hslToHex = (h, s, l) => {
s /= 100; l /= 100;
const a = s * Math.min(l, 1 - l);
const f = n => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0');
};
return '#' + f(0) + f(8) + f(4);
};
const baseColor = args.base_color || '#3b82f6';
const harmony = args.harmony || 'complementary';
const base = hexToHsl(baseColor);
const colors = { primary: baseColor };
switch (harmony) {
case 'complementary':
colors.secondary = hslToHex((base.h + 180) % 360, base.s, base.l);
colors.accent = hslToHex((base.h + 30) % 360, base.s, base.l);
break;
case 'analogous':
colors.secondary = hslToHex((base.h + 30) % 360, base.s, base.l);
colors.accent = hslToHex((base.h - 30 + 360) % 360, base.s, base.l);
break;
case 'triadic':
colors.secondary = hslToHex((base.h + 120) % 360, base.s, base.l);
colors.accent = hslToHex((base.h + 240) % 360, base.s, base.l);
break;
case 'split-complementary':
colors.secondary = hslToHex((base.h + 150) % 360, base.s, base.l);
colors.accent = hslToHex((base.h + 210) % 360, base.s, base.l);
break;
}
// Add neutrals
colors.background = hslToHex(base.h, 10, 98);
colors.surface = hslToHex(base.h, 10, 95);
colors.text = hslToHex(base.h, 10, 15);
colors.muted = hslToHex(base.h, 10, 45);
// Add primary shades
colors.primary_light = hslToHex(base.h, base.s, Math.min(base.l + 20, 95));
colors.primary_dark = hslToHex(base.h, base.s, Math.max(base.l - 20, 15));
return {
harmony,
palette: colors,
css_variables: Object.entries(colors).map(([k, v]) => \`--color-\${k.replace('_', '-')}: \${v};\`).join('\\n')
};`,
parameters: {
type: 'object',
properties: {
base_color: {
type: 'string',
description: 'Base color in hex format (e.g., #3b82f6)'
},
harmony: {
type: 'string',
description: 'Color harmony: complementary, analogous, triadic, split-complementary'
}
},
required: ['base_color']
}
},
// Python Templates
{
id: 'py-api-fetch',

View File

@@ -5,6 +5,17 @@
*/
import { promptsState, type Prompt } from '$lib/stores';
import {
getAllPromptTemplates,
getPromptCategories,
categoryInfo,
type PromptTemplate,
type PromptCategory
} from '$lib/prompts/templates';
// Tab state
type Tab = 'my-prompts' | 'browse-templates';
let activeTab = $state<Tab>('my-prompts');
// Editor state
let showEditor = $state(false);
@@ -18,6 +29,22 @@
let formTargetCapabilities = $state<string[]>([]);
let isSaving = $state(false);
// Template browser state
let selectedCategory = $state<PromptCategory | 'all'>('all');
let previewTemplate = $state<PromptTemplate | null>(null);
let addingTemplateId = $state<string | null>(null);
// Get templates and categories
const templates = getAllPromptTemplates();
const categories = getPromptCategories();
// Filtered templates
const filteredTemplates = $derived(
selectedCategory === 'all'
? templates
: templates.filter((t) => t.category === selectedCategory)
);
// Available capabilities for targeting
const CAPABILITIES = [
{ id: 'code', label: 'Code', description: 'Auto-use with coding models' },
@@ -82,7 +109,7 @@
function toggleCapability(capId: string): void {
if (formTargetCapabilities.includes(capId)) {
formTargetCapabilities = formTargetCapabilities.filter(c => c !== capId);
formTargetCapabilities = formTargetCapabilities.filter((c) => c !== capId);
} else {
formTargetCapabilities = [...formTargetCapabilities, capId];
}
@@ -110,6 +137,23 @@
}
}
async function addTemplateToLibrary(template: PromptTemplate): Promise<void> {
addingTemplateId = template.id;
try {
await promptsState.add({
name: template.name,
description: template.description,
content: template.content,
isDefault: false,
targetCapabilities: template.targetCapabilities
});
// Switch to My Prompts tab to show the new prompt
activeTab = 'my-prompts';
} finally {
addingTemplateId = null;
}
}
// Format date for display
function formatDate(date: Date): string {
return date.toLocaleDateString('en-US', {
@@ -123,7 +167,7 @@
<div class="h-full overflow-y-auto bg-theme-primary p-6">
<div class="mx-auto max-w-4xl">
<!-- Header -->
<div class="mb-8 flex items-center justify-between">
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-theme-primary">System Prompts</h1>
<p class="mt-1 text-sm text-theme-muted">
@@ -131,168 +175,449 @@
</p>
</div>
<button
type="button"
onclick={openCreateEditor}
class="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-theme-primary transition-colors hover:bg-blue-700"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
Create Prompt
</button>
</div>
<!-- Active prompt indicator -->
{#if promptsState.activePrompt}
<div class="mb-6 rounded-lg border border-blue-500/30 bg-blue-500/10 p-4">
<div class="flex items-center gap-2 text-sm text-blue-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Active system prompt for new chats: <strong class="text-blue-300">{promptsState.activePrompt.name}</strong></span>
</div>
</div>
{/if}
<!-- Prompts list -->
{#if promptsState.isLoading}
<div class="flex items-center justify-center py-12">
<div class="h-8 w-8 animate-spin rounded-full border-2 border-theme-subtle border-t-blue-500"></div>
</div>
{:else if promptsState.prompts.length === 0}
<div class="rounded-lg border border-dashed border-theme bg-theme-secondary/50 p-8 text-center">
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-theme-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
<h3 class="mt-4 text-sm font-medium text-theme-muted">No system prompts yet</h3>
<p class="mt-1 text-sm text-theme-muted">
Create a system prompt to customize AI behavior
</p>
{#if activeTab === 'my-prompts'}
<button
type="button"
onclick={openCreateEditor}
class="mt-4 inline-flex items-center gap-2 rounded-lg bg-theme-tertiary px-4 py-2 text-sm font-medium text-theme-primary transition-colors hover:bg-theme-tertiary"
class="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-theme-primary transition-colors hover:bg-blue-700"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
Create your first prompt
Create Prompt
</button>
</div>
{:else}
<div class="space-y-3">
{#each promptsState.prompts as prompt (prompt.id)}
<div
class="rounded-lg border bg-theme-secondary p-4 transition-colors {promptsState.activePromptId === prompt.id ? 'border-blue-500/50' : 'border-theme'}"
{/if}
</div>
<!-- Tabs -->
<div class="mb-6 flex gap-1 rounded-lg bg-theme-tertiary p-1">
<button
type="button"
onclick={() => (activeTab = 'my-prompts')}
class="flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors {activeTab ===
'my-prompts'
? 'bg-theme-secondary text-theme-primary shadow'
: 'text-theme-muted hover:text-theme-secondary'}"
>
My Prompts
{#if promptsState.prompts.length > 0}
<span
class="ml-1.5 rounded-full bg-theme-tertiary px-2 py-0.5 text-xs {activeTab ===
'my-prompts'
? 'bg-blue-500/20 text-blue-400'
: ''}"
>
<div class="flex items-start justify-between gap-4">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<h3 class="font-medium text-theme-primary">{prompt.name}</h3>
{#if prompt.isDefault}
<span class="rounded bg-blue-900 px-2 py-0.5 text-xs text-blue-300">
default
</span>
{/if}
{#if promptsState.activePromptId === prompt.id}
<span class="rounded bg-emerald-900 px-2 py-0.5 text-xs text-emerald-300">
active
</span>
{/if}
{#if prompt.targetCapabilities && prompt.targetCapabilities.length > 0}
{#each prompt.targetCapabilities as cap (cap)}
<span class="rounded bg-purple-900/50 px-2 py-0.5 text-xs text-purple-300">
{cap}
{promptsState.prompts.length}
</span>
{/if}
</button>
<button
type="button"
onclick={() => (activeTab = 'browse-templates')}
class="flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors {activeTab ===
'browse-templates'
? 'bg-theme-secondary text-theme-primary shadow'
: 'text-theme-muted hover:text-theme-secondary'}"
>
Browse Templates
<span
class="ml-1.5 rounded-full bg-theme-tertiary px-2 py-0.5 text-xs {activeTab ===
'browse-templates'
? 'bg-purple-500/20 text-purple-400'
: ''}"
>
{templates.length}
</span>
</button>
</div>
<!-- My Prompts Tab -->
{#if activeTab === 'my-prompts'}
<!-- Active prompt indicator -->
{#if promptsState.activePrompt}
<div class="mb-6 rounded-lg border border-blue-500/30 bg-blue-500/10 p-4">
<div class="flex items-center gap-2 text-sm text-blue-400">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span
>Active system prompt for new chats: <strong class="text-blue-300"
>{promptsState.activePrompt.name}</strong
></span
>
</div>
</div>
{/if}
<!-- Prompts list -->
{#if promptsState.isLoading}
<div class="flex items-center justify-center py-12">
<div
class="h-8 w-8 animate-spin rounded-full border-2 border-theme-subtle border-t-blue-500"
></div>
</div>
{:else if promptsState.prompts.length === 0}
<div
class="rounded-lg border border-dashed border-theme bg-theme-secondary/50 p-8 text-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mx-auto h-12 w-12 text-theme-muted"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
/>
</svg>
<h3 class="mt-4 text-sm font-medium text-theme-muted">No system prompts yet</h3>
<p class="mt-1 text-sm text-theme-muted">
Create a prompt or browse templates to get started
</p>
<div class="mt-4 flex justify-center gap-3">
<button
type="button"
onclick={openCreateEditor}
class="inline-flex items-center gap-2 rounded-lg bg-theme-tertiary px-4 py-2 text-sm font-medium text-theme-primary transition-colors hover:bg-theme-tertiary"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
Create from scratch
</button>
<button
type="button"
onclick={() => (activeTab = 'browse-templates')}
class="inline-flex items-center gap-2 rounded-lg bg-purple-600 px-4 py-2 text-sm font-medium text-theme-primary transition-colors hover:bg-purple-700"
>
Browse templates
</button>
</div>
</div>
{:else}
<div class="space-y-3">
{#each promptsState.prompts as prompt (prompt.id)}
<div
class="rounded-lg border bg-theme-secondary p-4 transition-colors {promptsState.activePromptId ===
prompt.id
? 'border-blue-500/50'
: 'border-theme'}"
>
<div class="flex items-start justify-between gap-4">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<h3 class="font-medium text-theme-primary">{prompt.name}</h3>
{#if prompt.isDefault}
<span class="rounded bg-blue-900 px-2 py-0.5 text-xs text-blue-300">
default
</span>
{/each}
{/if}
{#if promptsState.activePromptId === prompt.id}
<span class="rounded bg-emerald-900 px-2 py-0.5 text-xs text-emerald-300">
active
</span>
{/if}
{#if prompt.targetCapabilities && prompt.targetCapabilities.length > 0}
{#each prompt.targetCapabilities as cap (cap)}
<span class="rounded bg-purple-900/50 px-2 py-0.5 text-xs text-purple-300">
{cap}
</span>
{/each}
{/if}
</div>
{#if prompt.description}
<p class="mt-1 text-sm text-theme-muted">{prompt.description}</p>
{/if}
<p class="mt-2 line-clamp-2 text-sm text-theme-muted">
{prompt.content}
</p>
<p class="mt-2 text-xs text-theme-muted">
Updated {formatDate(prompt.updatedAt)}
</p>
</div>
{#if prompt.description}
<p class="mt-1 text-sm text-theme-muted">{prompt.description}</p>
{/if}
<p class="mt-2 line-clamp-2 text-sm text-theme-muted">
{prompt.content}
</p>
<p class="mt-2 text-xs text-theme-muted">
Updated {formatDate(prompt.updatedAt)}
</p>
</div>
<div class="flex items-center gap-2">
<!-- Use/Active toggle -->
<button
type="button"
onclick={() => handleSetActive(prompt)}
class="rounded p-1.5 transition-colors {promptsState.activePromptId === prompt.id ? 'bg-emerald-600 text-theme-primary' : 'text-theme-muted hover:bg-theme-tertiary hover:text-theme-primary'}"
title={promptsState.activePromptId === prompt.id ? 'Deactivate' : 'Use for new chats'}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</button>
<div class="flex items-center gap-2">
<!-- Use/Active toggle -->
<button
type="button"
onclick={() => handleSetActive(prompt)}
class="rounded p-1.5 transition-colors {promptsState.activePromptId === prompt.id
? 'bg-emerald-600 text-theme-primary'
: 'text-theme-muted hover:bg-theme-tertiary hover:text-theme-primary'}"
title={promptsState.activePromptId === prompt.id
? 'Deactivate'
: 'Use for new chats'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</button>
<!-- Set as default -->
<button
type="button"
onclick={() => handleSetDefault(prompt)}
class="rounded p-1.5 transition-colors {prompt.isDefault ? 'bg-blue-600 text-theme-primary' : 'text-theme-muted hover:bg-theme-tertiary hover:text-theme-primary'}"
title={prompt.isDefault ? 'Remove as default' : 'Set as default'}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill={prompt.isDefault ? 'currentColor' : 'none'} viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
</button>
<!-- Set as default -->
<button
type="button"
onclick={() => handleSetDefault(prompt)}
class="rounded p-1.5 transition-colors {prompt.isDefault
? 'bg-blue-600 text-theme-primary'
: 'text-theme-muted hover:bg-theme-tertiary hover:text-theme-primary'}"
title={prompt.isDefault ? 'Remove as default' : 'Set as default'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill={prompt.isDefault ? 'currentColor' : 'none'}
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"
/>
</svg>
</button>
<!-- Edit -->
<button
type="button"
onclick={() => openEditEditor(prompt)}
class="rounded p-1.5 text-theme-muted transition-colors hover:bg-theme-tertiary hover:text-theme-primary"
title="Edit"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<!-- Edit -->
<button
type="button"
onclick={() => openEditEditor(prompt)}
class="rounded p-1.5 text-theme-muted transition-colors hover:bg-theme-tertiary hover:text-theme-primary"
title="Edit"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
</button>
<!-- Delete -->
<button
type="button"
onclick={() => handleDelete(prompt)}
class="rounded p-1.5 text-theme-muted transition-colors hover:bg-red-900/30 hover:text-red-400"
title="Delete"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
<!-- Delete -->
<button
type="button"
onclick={() => handleDelete(prompt)}
class="rounded p-1.5 text-theme-muted transition-colors hover:bg-red-900/30 hover:text-red-400"
title="Delete"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
</div>
</div>
{/each}
</div>
{/if}
<!-- Info section -->
<section class="mt-8 rounded-lg border border-theme bg-theme-secondary/50 p-4">
<h3 class="flex items-center gap-2 text-sm font-medium text-theme-secondary">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
How System Prompts Work
</h3>
<p class="mt-2 text-sm text-theme-muted">
System prompts define the AI's behavior, personality, and constraints. They're sent at
the beginning of each conversation to set the context. Use them to create specialized
assistants (e.g., code reviewer, writing helper) or to enforce specific response formats.
</p>
<p class="mt-2 text-sm text-theme-muted">
<strong class="text-theme-secondary">Default prompt:</strong> Used for all new chats unless
overridden.
<strong class="text-theme-secondary">Active prompt:</strong> Currently selected for your session.
<strong class="text-theme-secondary">Capability targeting:</strong> Auto-matches prompts to
models with specific capabilities (code, vision, thinking, tools).
</p>
</section>
{/if}
<!-- Browse Templates Tab -->
{#if activeTab === 'browse-templates'}
<!-- Category filter -->
<div class="mb-6 flex flex-wrap gap-2">
<button
type="button"
onclick={() => (selectedCategory = 'all')}
class="rounded-lg px-3 py-1.5 text-sm font-medium transition-colors {selectedCategory ===
'all'
? 'bg-theme-secondary text-theme-primary'
: 'bg-theme-tertiary text-theme-muted hover:text-theme-secondary'}"
>
All
</button>
{#each categories as category (category)}
{@const info = categoryInfo[category]}
<button
type="button"
onclick={() => (selectedCategory = category)}
class="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors {selectedCategory ===
category
? info.color
: 'bg-theme-tertiary text-theme-muted hover:text-theme-secondary'}"
>
<span>{info.icon}</span>
{info.label}
</button>
{/each}
</div>
<!-- Templates grid -->
<div class="grid gap-4 sm:grid-cols-2">
{#each filteredTemplates as template (template.id)}
{@const info = categoryInfo[template.category]}
<div class="rounded-lg border border-theme bg-theme-secondary p-4">
<div class="mb-3 flex items-start justify-between gap-3">
<div>
<h3 class="font-medium text-theme-primary">{template.name}</h3>
<span class="mt-1 inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs {info.color}">
<span>{info.icon}</span>
{info.label}
</span>
</div>
<button
type="button"
onclick={() => addTemplateToLibrary(template)}
disabled={addingTemplateId === template.id}
class="flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-theme-primary transition-colors hover:bg-blue-700 disabled:opacity-50"
>
{#if addingTemplateId === template.id}
<svg class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
{/if}
Add
</button>
</div>
<p class="text-sm text-theme-muted">{template.description}</p>
<button
type="button"
onclick={() => (previewTemplate = template)}
class="mt-3 text-sm text-blue-400 hover:text-blue-300"
>
Preview prompt
</button>
</div>
{/each}
</div>
{/if}
<!-- Info section -->
<section class="mt-8 rounded-lg border border-theme bg-theme-secondary/50 p-4">
<h3 class="flex items-center gap-2 text-sm font-medium text-theme-secondary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
How System Prompts Work
</h3>
<p class="mt-2 text-sm text-theme-muted">
System prompts define the AI's behavior, personality, and constraints. They're sent at the
beginning of each conversation to set the context. Use them to create specialized assistants
(e.g., code reviewer, writing helper) or to enforce specific response formats.
</p>
<p class="mt-2 text-sm text-theme-muted">
<strong class="text-theme-secondary">Default prompt:</strong> Used for all new chats unless overridden.
<strong class="text-theme-secondary">Active prompt:</strong> Currently selected for your session.
<strong class="text-theme-secondary">Capability targeting:</strong> Auto-matches prompts to models with specific capabilities (code, vision, thinking, tools).
</p>
</section>
<!-- Info about templates -->
<section class="mt-8 rounded-lg border border-theme bg-theme-secondary/50 p-4">
<h3 class="flex items-center gap-2 text-sm font-medium text-theme-secondary">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 text-purple-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 14v3m4-3v3m4-3v3M3 21h18M3 10h18M3 7l9-4 9 4M4 10h16v11H4V10z"
/>
</svg>
About Templates
</h3>
<p class="mt-2 text-sm text-theme-muted">
These curated templates are designed for common use cases. When you add a template, it
creates a copy in your library that you can customize. Templates with capability tags
will auto-match with compatible models.
</p>
</section>
{/if}
</div>
</div>
@@ -300,8 +625,12 @@
{#if showEditor}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onclick={(e) => { if (e.target === e.currentTarget) closeEditor(); }}
onkeydown={(e) => { if (e.key === 'Escape') closeEditor(); }}
onclick={(e) => {
if (e.target === e.currentTarget) closeEditor();
}}
onkeydown={(e) => {
if (e.key === 'Escape') closeEditor();
}}
role="dialog"
aria-modal="true"
aria-labelledby="editor-title"
@@ -316,13 +645,26 @@
onclick={closeEditor}
class="rounded p-1 text-theme-muted transition-colors hover:bg-theme-tertiary hover:text-theme-primary"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form onsubmit={(e) => { e.preventDefault(); handleSave(); }} class="p-6">
<form
onsubmit={(e) => {
e.preventDefault();
handleSave();
}}
class="p-6"
>
<div class="space-y-4">
<!-- Name -->
<div>
@@ -341,7 +683,10 @@
<!-- Description -->
<div>
<label for="prompt-description" class="mb-1 block text-sm font-medium text-theme-secondary">
<label
for="prompt-description"
class="mb-1 block text-sm font-medium text-theme-secondary"
>
Description
</label>
<input
@@ -390,14 +735,19 @@
Auto-use for model types
</label>
<p class="mb-3 text-xs text-theme-muted">
When a model has these capabilities and no other prompt is selected, this prompt will be used automatically.
When a model has these capabilities and no other prompt is selected, this prompt will
be used automatically.
</p>
<div class="flex flex-wrap gap-2">
{#each CAPABILITIES as cap (cap.id)}
<button
type="button"
onclick={() => toggleCapability(cap.id)}
class="rounded-lg border px-3 py-1.5 text-sm transition-colors {formTargetCapabilities.includes(cap.id) ? 'border-blue-500 bg-blue-500/20 text-blue-300' : 'border-theme-subtle bg-theme-tertiary text-theme-muted hover:border-theme hover:text-theme-secondary'}"
class="rounded-lg border px-3 py-1.5 text-sm transition-colors {formTargetCapabilities.includes(
cap.id
)
? 'border-blue-500 bg-blue-500/20 text-blue-300'
: 'border-theme-subtle bg-theme-tertiary text-theme-muted hover:border-theme hover:text-theme-secondary'}"
title={cap.description}
>
{cap.label}
@@ -428,3 +778,94 @@
</div>
</div>
{/if}
<!-- Template Preview Modal -->
{#if previewTemplate}
{@const info = categoryInfo[previewTemplate.category]}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onclick={(e) => {
if (e.target === e.currentTarget) previewTemplate = null;
}}
onkeydown={(e) => {
if (e.key === 'Escape') previewTemplate = null;
}}
role="dialog"
aria-modal="true"
>
<div class="w-full max-w-2xl max-h-[80vh] flex flex-col rounded-xl bg-theme-secondary shadow-xl">
<div class="flex items-center justify-between border-b border-theme px-6 py-4">
<div>
<h2 class="text-lg font-semibold text-theme-primary">{previewTemplate.name}</h2>
<div class="mt-1 flex items-center gap-2">
<span class="inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs {info.color}">
<span>{info.icon}</span>
{info.label}
</span>
{#if previewTemplate.targetCapabilities}
{#each previewTemplate.targetCapabilities as cap}
<span class="rounded bg-purple-900/50 px-2 py-0.5 text-xs text-purple-300">
{cap}
</span>
{/each}
{/if}
</div>
</div>
<button
type="button"
onclick={() => (previewTemplate = null)}
class="rounded p-1 text-theme-muted transition-colors hover:bg-theme-tertiary hover:text-theme-primary"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-1 overflow-y-auto p-6">
<p class="mb-4 text-sm text-theme-muted">{previewTemplate.description}</p>
<pre
class="whitespace-pre-wrap rounded-lg bg-theme-tertiary p-4 font-mono text-sm text-theme-primary">{previewTemplate.content}</pre>
</div>
<div class="flex justify-end gap-3 border-t border-theme px-6 py-4">
<button
type="button"
onclick={() => (previewTemplate = null)}
class="rounded-lg px-4 py-2 text-sm font-medium text-theme-secondary transition-colors hover:bg-theme-tertiary"
>
Close
</button>
<button
type="button"
onclick={() => {
if (previewTemplate) {
addTemplateToLibrary(previewTemplate);
previewTemplate = null;
}
}}
class="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-theme-primary transition-colors hover:bg-blue-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
Add to Library
</button>
</div>
</div>
</div>
{/if}