Moved pattern loader to ModelConfig. Editing styles in chat/. Added page fly transitions. Tidying. Removed - ChatHeader, unused modal from Transcripts, FlyandScaleParams from lib/types/utils.

This commit is contained in:
John
2024-12-03 02:04:39 -05:00
parent 9fa8634083
commit f33ebb7e25
16 changed files with 220 additions and 196 deletions

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { cn } from "$lib/types/utils";
let className: string | undefined = undefined;
export let value = undefined;
export let value: string | undefined = undefined;
export { className as class };
export let readonly = undefined;
</script>

View File

@@ -1,5 +1,5 @@
<script>
import { cn } from "$lib/types/utils.ts";
<script lang="ts">
import { cn } from "$lib/types/utils";
let className = undefined;
export { className as class };
</script>

View File

@@ -1,13 +1,13 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { cubicOut } from 'svelte/easing';
import type { TransitionConfig } from 'svelte/transition';
// import { cubicOut } from 'svelte/easing';
// import type { TransitionConfig } from 'svelte/transition';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
/* type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
@@ -53,4 +53,4 @@ export const flyAndScale = (
},
easing: cubicOut
};
};
}; */

View File

@@ -4,6 +4,8 @@
import Footer from './Footer.svelte';
import Header from './Header.svelte';
import { initializeStores } from '@skeletonlabs/skeleton';
import { page } from '$app/stores';
import { fly } from 'svelte/transition';
import { getToastStore } from '@skeletonlabs/skeleton';
import { onMount } from 'svelte';
@@ -14,9 +16,9 @@
onMount(() => {
toastStore.trigger({
message: "👋 Welcome to the site! I'm still working on it... ",
message: "👋 Welcome to the site! Tell people about yourself and what you do.",
background: 'variant-filled-primary',
timeout: 15000,
timeout: 3333,
hoverable: true
});
});
@@ -24,6 +26,7 @@
<Toast position="t" />
{#key $page.url.pathname}
<AppShell class="relative">
<div class="fixed inset-0 bg-gradient-to-br from-primary-500/20 via-tertiary-500/20 to-secondary-500/20 -z-10"></div>
<svelte:fragment slot="header">
@@ -31,13 +34,18 @@
<div class="bg-gradient-to-b variant-gradient-primary-tertiary opacity-20 h-2 py-4">
</div>
</svelte:fragment>
<div
in:fly={{ duration: 1000, delay: 300, y: 100 }}
>
<main class="mx-auto p-4">
<slot />
</main>
</div>
<svelte:fragment slot="footer">
<Footer />
</svelte:fragment>
</AppShell>
{/key}
<style>
main {

View File

@@ -3,7 +3,7 @@
import Fabric from './Fabric.svelte';
</script>
<div class="">
<div>
<Terminal />
<div class="absolute inset-0 -z-10 overflow-hidden h-96">
<Fabric />
@@ -11,3 +11,4 @@
</div>

View File

@@ -2,6 +2,7 @@
import { onMount, onDestroy } from 'svelte';
import { ParticleSystem } from './ParticleSystem';
import { createParticleGradient } from '$lib/utils/canvas';
import { fade } from 'svelte/transition';
export let particleCount = 100;
export let particleSize = 3;

View File

@@ -6,11 +6,11 @@
<footer class="border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div class="container flex h-14 items-center justify-between px-4">
<p class="text-sm text-muted-foreground">
Built in {year} by @johnconnor-sec
Built in {year} by @Fabric <!-- Feel free to put your name here -->
</p>
<nav class="flex items-center gap-4 ">
<BuyMeCoffee url="https://www.buymeacoffee.com/johnconnor.sec" />
<BuyMeCoffee url="https://www.buymeacoffee.com/johnconnor.sec" /> <!-- And here -->
</nav>
</div>
</footer>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
// import { fade } from 'svelte/transition';
import { goto } from '$app/navigation';
let mounted = false;
@@ -98,7 +98,7 @@
});
</script>
<div class="min-h-[calc(100vh-4rem)] pt-20 pb-8 px-4">
<div class="pt-20 pb-8 px-4">
<div class="container mx-auto max-w-4xl">
<div class="terminal-window backdrop-blur-sm">
<!-- Terminal header -->
@@ -111,14 +111,12 @@
<span class="text-sm text-gray-400 ml-2">me@localhost</span>
</div>
<!-- Terminal content -->
<div class="p-6">
<!-- Terminal output -->
<div class="mb-4 whitespace-pre-wrap terminal-text leading-relaxed">{terminalContent}</div>
<!-- Command input -->
{#if mounted}
<div class="flex items-center command-input" in:fade={{ duration: 200 }}>
<div class="flex items-center command-input">
<span class="mr-2 terminal-prompt font-bold">$</span>
<!-- {#if showCursor}
<span class="animate-blink terminal-text">▋</span>
@@ -132,16 +130,6 @@
/>
</div>
{/if}
<!-- Command history -->
<!-- <div class="mt-6 space-y-1 text-sm text-gray-500">
{#each commandHistory as cmd}
<div class="terminal-text opacity-60">
<span class="terminal-prompt font-bold mr-2">$</span>
{cmd}
</div>
{/each}
</div> -->
</div>
</div>
</div>

View File

@@ -1,20 +1,10 @@
<script lang="ts">
import ChatHeader from './ChatHeader.svelte';
import ChatInput from "./ChatInput.svelte";
import ChatMessages from "./ChatMessages.svelte";
import ModelConfig from "./ModelConfig.svelte";
//import { Button } from "$lib/components/ui/button";
//import { setSystemPrompt } from "$lib/store/chat";
// import { Select } from "$lib/components/ui/select";
// import { Save, Code, Share2, MoreHorizontal } from 'lucide-svelte';
// import { onMount } from "svelte";
</script>
<ChatHeader />
<div class="flex-1 container mx-auto p-4">
<div class="flex flex-align-vertical mx-auto p-4">
<div class="grid grid-cols-1 lg:grid-cols-[minmax(400px,_600px),minmax(400px,_600px),300px] gap-4">
<div class="space-y-4 order-2 lg:order-1 h-full overflow-y flex-1">
<ChatInput />

View File

@@ -1,22 +0,0 @@
<!-- Moved to ModelConfig.svelte -->
<!-- <script lang="ts">
import { onMount } from 'svelte';
import { availableModels, modelConfig, loadAvailableModels } from '$lib/store/model-config';
onMount(async () => {
await loadAvailableModels();
});
</script>
<select
class="flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 appearance-none"
bind:value={$modelConfig.model}
>
<option value="">Select a model...</option>
{#each $availableModels as model}
<option value={model.name}>
{model.name} ({model.vendor})
</option>
{/each}
</select> -->

View File

@@ -1,36 +0,0 @@
<script lang="ts">
import { patterns } from "$lib/types/chat/patterns";
import { patternAPI } from "$lib/types/chat/patterns";
import { Select } from "$lib/components/ui/select";
import { onMount } from "svelte";
let selectedPreset = "";
$: if (selectedPreset) {
console.log('Pattern selected:', selectedPreset);
patternAPI.selectPattern(selectedPreset);
}
onMount(async () => {
await patternAPI.loadPatterns();
});
</script>
<header class="border-b">
<div class="container mx-auto p-3">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h1 class="text-xl font-semibold">Chat</h1>
<div class="flex flex-wrap items-center gap-2 w-full sm:w-auto">
<Select
class="w-full font-bold sm:w-[200px]"
bind:value={selectedPreset}
>
<option value="">Load a pattern...</option>
{#each $patterns as pattern}
<option value={pattern.Name}>{pattern.Description}</option>
{/each}
</Select>
</div>
</div>
</div>
</header>

View File

@@ -2,6 +2,7 @@
import { Button } from "$lib/components/ui/button";
import { Textarea } from "$lib/components/ui/textarea";
import { Label } from "$lib/components/ui/label";
// import { FileButton } from '@skeletonlabs/skeleton';
import { currentSession, setSession, sendMessage } from '$lib/store/chat';
import { systemPrompt } from '$lib/types/chat/patterns';
import { onMount } from 'svelte';
@@ -49,10 +50,11 @@
<div class="flex flex-col h-full">
<div class="space-y-2 flex-none">
<Label class="p-1 font-bold">System Prompt</Label>
<Label
class="p-1 font-bold">System Prompt</Label>
<Textarea
value={$systemPrompt}
on:input={(e) => systemPrompt}
on:input={(e) => $systemPrompt}
placeholder="Enter system instructions..."
class="min-h-[200px] resize-none bg-background"
/>
@@ -60,13 +62,14 @@
<div class="space-y-2 flex-1 py-1">
<Label class="p-1 font-bold">User Input</Label>
<Textarea
bind:value={userInput}
placeholder="Enter your message..."
class="h-[calc(80vh-24rem)] resize-none bg-background"
/>
<Textarea
bind:value={userInput}
on:input={(e) => userInput}
placeholder="Enter your message..."
class="h-[calc(80vh-24rem)] resize-none bg-background"
/>
</div>
<Button on:click={handleSubmit} variant="outline" size="sm" class="mt-2">Submit</Button>
<Button on:click={handleSubmit} variant="outline" size="sm">Submit</Button>
</div>
</div>

View File

@@ -1,9 +1,12 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { RotateCcw, Trash2, Save } from 'lucide-svelte';
import { RotateCcw, Trash2, Save, Copy } from 'lucide-svelte';
import { chatState, clearMessages, revertLastMessage, currentSession } from '$lib/store/chat';
import { afterUpdate } from 'svelte';
import { marked } from 'marked';
import { getToastStore } from '@skeletonlabs/skeleton';
import { Toast } from '@skeletonlabs/skeleton';
let sessionName: string | null = null;
let messagesContainer: HTMLDivElement;
@@ -29,19 +32,55 @@
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
marked.setOptions({
gfm: true,
breaks: true,
});
function renderMarkdown(content: string, isAssistant: boolean) {
if (!isAssistant) return content;
try {
return marked.parse(content);
} catch (error) {
console.error('Error rendering markdown:', error);
return content;
}
}
const toastStore = getToastStore();
async function copyToClipboard() {
try {
await navigator.clipboard.writeText($chatState.messages.map(m => m.content).join('\n'));
toastStore.trigger({
message: 'Chat copied to clipboard!',
background: 'variant-filled-success'
});
} catch (err) {
toastStore.trigger({
message: 'Failed to copy transcript',
background: 'variant-filled-error'
});
}
}
</script>
<div class="chat-messages-wrapper flex flex-col h-full">
<div class="flex justify-between items-center mb-4 flex-none">
<span class="text-sm font-medium">Chat History</span>
<div class="flex gap-2">
<Button variant="outline" size="icon" on:click={revertLastMessage}>
<Button variant="outline" size="icon" aria-label="Revert Last Message" on:click={revertLastMessage}>
<RotateCcw class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" on:click={clearMessages}>
<Button variant="outline" size="icon" aria-label="Clear Chat" on:click={clearMessages}>
<Trash2 class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" on:click={saveChat}>
<Button variant="outline" size="icon" aria-label="Copy Chat" on:click={copyToClipboard}>
<Copy class="h-4 w-4" />
</Button>
<Toast position="b" />
<Button variant="outline" size="icon" aria-label="Save Chat" on:click={saveChat}>
<Save class="h-4 w-4" />
</Button>
</div>
@@ -50,9 +89,15 @@
<div class="messages-container flex-1" bind:this={messagesContainer}>
<div class="messages-content space-y-4">
{#each $chatState.messages as message}
<div class="message-item whitespace-pre-wrap text-sm {message.role === 'assistant' ? 'pl-4' : 'font-medium'} transition-all">
<div class="message-item {message.role === 'assistant' ? 'pl-4' : 'font-medium'} transition-all">
<span class="text-xs tertiary uppercase">{message.role}:</span>
{message.content}
{#if message.role === 'assistant'}
{@html renderMarkdown(message.content, true)}
{:else}
<div class="whitespace-pre-wrap text-sm">
{message.content}
</div>
{/if}
</div>
{/each}
{#if $chatState.isStreaming}
@@ -97,4 +142,50 @@
background-color: var(--color-primary-500);
border-radius: 2px;
}
/* Markdown content styles */
:global(.message-item.pl-4) {
font-size: 0.875rem;
}
:global(.message-item pre) {
background-color: rgb(var(--color-secondary-50));
padding: 1rem;
border-radius: 0.5rem;
margin: 0.5rem 0;
overflow-x: auto;
}
:global(.message-item code) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.875rem;
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
background-color: rgb(var(--color-secondary-50));
}
:global(.message-item p) {
margin: 0.5rem 0;
}
:global(.message-item ul, .message-item ol) {
margin: 0.5rem 0;
padding-left: 1.5rem;
}
:global(.message-item li) {
margin: 0.25rem 0;
}
:global(.message-item a) {
color: rgb(var(--color-primary-600));
text-decoration: underline;
}
:global(.message-item blockquote) {
border-left: 4px solid rgb(var(--color-secondary-200));
margin: 0.5rem 0;
padding-left: 1rem;
font-style: italic;
}
</style>

View File

@@ -5,57 +5,87 @@
import { Slider } from "$lib/components/ui/slider";
import { modelConfig, availableModels, loadAvailableModels } from "$lib/store/model-config";
import Transcripts from "./Transcripts.svelte";
import { patterns } from "$lib/types/chat/patterns";
import { patternAPI } from "$lib/types/chat/patterns";
let selectedPreset = "";
$: if (selectedPreset) {
console.log('Pattern selected:', selectedPreset);
patternAPI.selectPattern(selectedPreset);
}
onMount(async () => {
await loadAvailableModels();
await patternAPI.loadPatterns();
});
//Debugging
//$: console.log('Current available models:', $availableModels);
//$: console.log('Current model config:', $modelConfig);
</script>
<div class="space-y-4">
<div class="space-y-2">
<Label>Model</Label>
<Select bind:value={$modelConfig.model}>
<option value="">Default Model</option>
{#each $availableModels as model (model.name)}
<option value={model.name}>{model.vendor} - {model.name}</option>
{/each}
</Select>
</div>
<div class="flex gap-4">
<div class="flex-1">
<Select
bind:value={selectedPreset}
>
<option value="">Load a pattern...</option>
{#each $patterns as pattern}
<option value={pattern.Name}>{pattern.Description}</option>
{/each}
</Select>
</div>
<div class="flex-1">
<Select
bind:value={$modelConfig.model}
>
<option value="">Default Model</option>
{#each $availableModels as model (model.name)}
<option value={model.name}>{model.vendor} - {model.name}</option>
{/each}
</Select>
</div>
</div>
<div class="space-y-2">
<Label class="p-1 font-bold">Temperature ({$modelConfig.temperature.toFixed(1)})</Label>
<Slider
bind:value={$modelConfig.temperature}
min={0}
max={2}
step={0.1}
/>
</div>
<div class="space-y-2">
<Label>Temperature ({$modelConfig.temperature.toFixed(1)})</Label>
<Slider
bind:value={$modelConfig.temperature}
min={0}
max={2}
step={0.1}
/>
</div>
<div class="space-y-2">
<Label class="p-1 font-bold">Maximum Length ({$modelConfig.maxLength})</Label>
<Slider
bind:value={$modelConfig.maxLength}
min={1}
max={4000}
step={1}
/>
</div>
<div class="space-y-2">
<Label>Maximum Length ({$modelConfig.maxLength})</Label>
<Slider
bind:value={$modelConfig.maxLength}
min={1}
max={4000}
step={1}
/>
</div>
<div class="space-y-2">
<Label class="p-1 font-bold">Top P ({$modelConfig.top_p.toFixed(2)})</Label>
<Slider
bind:value={$modelConfig.top_p}
min={0}
max={1}
step={0.01}
/>
</div>
<div class="space-y-2">
<Label>Top P ({$modelConfig.top_p.toFixed(2)})</Label>
<Slider
bind:value={$modelConfig.top_p}
min={0}
max={1}
step={0.01}
/>
</div>
<div class="space-y-2">
<Transcripts />
</div>
<div class="space-y-2">
<Label class="p-1 font-bold">Frequency Penalty ({$modelConfig.frequency.toFixed(2)})</Label>
<Slider
bind:value={$modelConfig.frequency}
min={0}
max={1}
step={0.01}
/>
</div>
<div class="space-y-2">
<Transcripts />
</div>
</div>

View File

@@ -2,8 +2,8 @@
import { getToastStore } from '@skeletonlabs/skeleton';
import { Button } from "$lib/components/ui/button";
import Input from '$components/ui/input/input.svelte';
import { getModalStore, type ModalSettings } from '@skeletonlabs/skeleton';
import { Toast } from '@skeletonlabs/skeleton';
let url = '';
let transcript = '';
let loading = false;
@@ -11,13 +11,13 @@
let title = '';
const toastStore = getToastStore();
const modalStore = getModalStore();
// Event dispatcher to send transcript to parent
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
async function fetchTranscript() {
function isValidYouTubeUrl(url: string) {
const pattern = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/;
return pattern.test(url);
}
if (!isValidYouTubeUrl(url)) {
error = 'Please enter a valid YouTube URL';
toastStore.trigger({
@@ -50,42 +50,12 @@
transcript = data.transcript;
title = data.title;
// Show modal with transcript
const modal: ModalSettings = {
type: 'component',
title: 'YouTube Transcript',
body: transcript,
buttonTextCancel: 'Close',
buttonTextSubmit: 'Use in Chat',
response: (r: boolean) => {
if (r) {
dispatch('transcript', transcript);
}
}
};
modalStore.trigger(modal);
toastStore.trigger({
message: 'Transcript fetched successfully!',
background: 'variant-filled-success'
});
} catch (err) {
error = err.message;
toastStore.trigger({
message: error,
background: 'variant-filled-error'
});
} finally {
loading = false;
}
}
function isValidYouTubeUrl(url) {
const pattern = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/;
return pattern.test(url);
}
async function copyToClipboard() {
try {
await navigator.clipboard.writeText(transcript);
@@ -129,6 +99,7 @@
{/if}
{#if transcript}
<Toast position="b" />
<div class="space-y-4 border rounded-lg p-4 bg-muted/50 h-96">
<div class="flex justify-between items-center">
<h3 class="text-xs font-semibold">{title || 'Transcript'}</h3>

View File

@@ -1,14 +1,13 @@
<script lang="ts">
import { formatDistance } from 'date-fns';
import type { PageData } from './$types';
import { Paginator } from '@skeletonlabs/skeleton'
// import { Paginator } from '@skeletonlabs/skeleton'
// import Spinner from '$lib/components/ui/spinner/spinner.svelte';
export let data: PageData;
$: posts = data.posts;
let visible: boolean = true;
let message: string = "No posts found";
</script>
<div class="container py-12">