278 lines
9.2 KiB
Svelte
278 lines
9.2 KiB
Svelte
<script lang="ts">
|
|
import { preventDefault } from 'svelte/legacy';
|
|
|
|
import '../../app.css';
|
|
import type { SearchResult } from '$lib/utils/search';
|
|
import { searchResults, type ChatHistory } from '$lib/store';
|
|
import { onMount } from 'svelte';
|
|
import { marked } from 'marked';
|
|
import { HumanMessage, AIMessage } from '@langchain/core/messages';
|
|
import { PUBLIC_LOAD_DUMMY_HISTORY } from '$env/static/public';
|
|
import Toast from '$lib/components/Toast.svelte';
|
|
|
|
let searchResultsValue: SearchResult[] = $state([]);
|
|
let query = $state('');
|
|
let loading = $state(false);
|
|
let showToast = $state(false);
|
|
let toastMessage = $state('');
|
|
|
|
type Questions = {
|
|
[key: string]: string[];
|
|
};
|
|
|
|
const QUESTIONS: Questions = {
|
|
brewing: [
|
|
'What is the difference between Western and Eastern styles of tea brewing?',
|
|
'How does water temperature and steeping time affect the flavor of different types of tea?',
|
|
'What are some traditional tea brewing methods from around the world?',
|
|
'What is gongfu cha (功夫茶), and how does it differ from other tea preparation methods?'
|
|
],
|
|
varieties: [
|
|
'What is the difference between raw and ripe pu-erh tea, and how does aging affect their taste and value?',
|
|
'Can you explain the grading system for teas, particularly for varieties like Darjeeling and Assam?',
|
|
'What are some rare or unusual tea varieties and what makes them unique?',
|
|
'How do flavored teas (like Earl Grey or Jasmine) differ in production and taste from pure teas?'
|
|
],
|
|
history: [
|
|
'How did tea influence global trade routes and international relations, particularly between China, Britain, and India?',
|
|
'What role did Buddhism play in the spread of tea culture throughout Asia?',
|
|
"How did the British East India Company's involvement in the tea trade impact both India and China?",
|
|
'How did the Opium Wars, which were closely tied to the tea trade, alter the course of Chinese history?',
|
|
'What role did tea play in the American Revolution, and how did this event change tea consumption patterns in America?'
|
|
],
|
|
culture: [
|
|
'In what ways did tea ceremonies in different cultures (like Japan and England) reflect and shape social norms?',
|
|
'How did the democratization of tea drinking in Europe affect social structures and daily life?',
|
|
'How has the perception of tea as a medicinal substance evolved from ancient times to the present day?',
|
|
'What are some of the most popular tea-related idioms and proverbs, and what do they mean?'
|
|
],
|
|
production: [
|
|
'How did the processing and preparation of tea evolve over time, and what factors influenced these changes?',
|
|
'What impact did tea plantations have on local ecosystems and labor practices in countries like India and Sri Lanka?',
|
|
'What are the main differences in production methods between black, green, oolong, and white teas?',
|
|
'How does the terroir (environmental factors) affect the flavor and quality of tea?'
|
|
]
|
|
};
|
|
|
|
function generateDummyHistory() {
|
|
return [
|
|
JSON.parse(JSON.stringify(new HumanMessage({ content: 'Hello, AI!' }))),
|
|
JSON.parse(JSON.stringify(new AIMessage({ content: 'Hello! How can I assist you today?' }))),
|
|
JSON.parse(JSON.stringify(new HumanMessage({ content: "What's the weather like?" }))),
|
|
JSON.parse(
|
|
JSON.stringify(
|
|
new AIMessage({
|
|
content:
|
|
"I'm sorry, but I don't have access to real-time weather information. You might want to check a weather app or website for the most up-to-date forecast."
|
|
})
|
|
)
|
|
)
|
|
];
|
|
}
|
|
|
|
let chatHistory: ChatHistory = $state([]);
|
|
|
|
onMount(async () => {
|
|
try {
|
|
if (PUBLIC_LOAD_DUMMY_HISTORY === 'true') {
|
|
chatHistory = generateDummyHistory();
|
|
return;
|
|
}
|
|
const response = await fetch('/api/ai');
|
|
const data = await response.json();
|
|
chatHistory = data.chatHistory || [];
|
|
} catch (error) {
|
|
console.error('Error fetching chat history:', error);
|
|
}
|
|
});
|
|
|
|
searchResults.subscribe((value: SearchResult[]) => {
|
|
searchResultsValue = value ? value : [];
|
|
});
|
|
|
|
function getRoleAndContent(message: any): { role: string; content: string } {
|
|
if (message.type === 'constructor') {
|
|
const messageType = message.id[2];
|
|
return {
|
|
role: messageType.replace('Message', '').toLowerCase(),
|
|
content: message.kwargs.content
|
|
};
|
|
}
|
|
return {
|
|
role: 'unknown',
|
|
content: JSON.stringify(message)
|
|
};
|
|
}
|
|
|
|
// safely render markdown
|
|
function renderMarkdown(content: string) {
|
|
return marked(content);
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
loading = true;
|
|
try {
|
|
const response = await fetch('/api/ai', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ query })
|
|
});
|
|
const data = await response.json();
|
|
chatHistory = data.chatHistory || [];
|
|
|
|
query = '';
|
|
} catch (error) {
|
|
console.error('Error fetching AI response:', error);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
async function handleNewSession() {
|
|
try {
|
|
const response = await fetch('/api/ai/new-session', { method: 'POST' });
|
|
if (response.ok) {
|
|
const historyResponse = await fetch('/api/ai');
|
|
const data = await historyResponse.json();
|
|
chatHistory = data.chatHistory || [];
|
|
query = '';
|
|
} else {
|
|
console.error('Failed to start new session');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error starting new session:', error);
|
|
}
|
|
}
|
|
|
|
function copyToClipboard(text: string) {
|
|
navigator.clipboard
|
|
.writeText(text)
|
|
.then(() => {
|
|
toastMessage = 'Copied to clipboard!';
|
|
showToast = true;
|
|
})
|
|
.catch((err) => {
|
|
toastMessage = 'Failed to copy text.';
|
|
showToast = true;
|
|
});
|
|
}
|
|
|
|
function handleToastClose() {
|
|
showToast = false;
|
|
}
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>silentsilas - AI</title>
|
|
</svelte:head>
|
|
|
|
{#if searchResultsValue.length === 0}
|
|
<div class="flex-grow flex-col overflow-auto p-4 my-2 bg-base-200">
|
|
<div class="flex flex-col items-center gap-4">
|
|
<div class="avatar">
|
|
<div class="ring-primary ring-offset-base-100 w-64 rounded-full ring ring-offset-2">
|
|
<img src="/imgs/ai/profile.jpg" alt="Portrait of an orange tabby cat reading a book" />
|
|
</div>
|
|
</div>
|
|
<span>
|
|
This lil guy just finished reading <a
|
|
class="link link-primary"
|
|
href="https://bookshop.org/p/books/a-history-of-tea-the-life-and-times-of-the-world-s-favorite-beverage-laura-c-martin/11044690"
|
|
target="_blank"
|
|
>
|
|
A History of Tea
|
|
</a>, so ask 'em about tea or something.
|
|
</span>
|
|
</div>
|
|
<div class="space-y-4">
|
|
{#each chatHistory as message}
|
|
{@const { role, content } = getRoleAndContent(message)}
|
|
{#if role === 'human'}
|
|
<div class="chat chat-end">
|
|
<div class="chat-bubble chat-bubble-primary prose">
|
|
{@html renderMarkdown(content)}
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div class="p-4 rounded-lg bg-base-300 prose md:container">
|
|
{@html renderMarkdown(content)}
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
|
|
{#if loading}
|
|
<div class="mt-4 flex flex-col items-center justify-center">
|
|
<span class="loading loading-dots loading-lg"></span>
|
|
<span>This may take a minute; streaming is still a work in progress.</span>
|
|
</div>
|
|
{/if}
|
|
<form onsubmit={preventDefault(handleSubmit)} class="mt-4 flex-col">
|
|
<label class="form-control">
|
|
<textarea
|
|
bind:value={query}
|
|
class="textarea textarea-bordered h-24"
|
|
placeholder="Type your message here..."
|
|
></textarea>
|
|
</label>
|
|
<button type="submit" class="btn btn-block btn-primary mt-4" disabled={loading}>
|
|
Send
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-block btn-error btn-outline mt-2"
|
|
onclick={handleNewSession}
|
|
>
|
|
New Session
|
|
</button>
|
|
<div class="bg-base-200 collapse collapse-plus border-primary border mt-20">
|
|
<input type="checkbox" class="peer" />
|
|
<div class="collapse-title bg-base-200 peer-checked:bg-base-300">
|
|
Interesting things to ask:
|
|
</div>
|
|
<div class="collapse-content bg-base-200 peer-checked:bg-base-300">
|
|
{#each Object.entries(QUESTIONS) as [category, questions]}
|
|
<div class="mb-4">
|
|
<h3 class="text-lg font-semibold mb-2 capitalize">{category}</h3>
|
|
<ul class="list-disc list-inside space-y-4">
|
|
{#each questions as item}
|
|
<li class="flex items-center gap-2">
|
|
<span>{item}</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-outline"
|
|
onclick={() => copyToClipboard(item)}
|
|
title="Copy to clipboard"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.5"
|
|
stroke="currentColor"
|
|
class="w-4 h-4"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if showToast}
|
|
<Toast message={toastMessage} type="success" on:close={handleToastClose} />
|
|
{/if}
|