Add project save/load and improve AI chat formatting
Project persistence: - save_project_transcript command: persists segments, speakers, words to SQLite - load_project_transcript command: loads full transcript with nested words - delete_project command: soft-delete projects - Auto-save after pipeline completes (named from filename) - Project dropdown in header to switch between saved transcripts - Projects load audio, segments, and speakers from database AI chat improvements: - Markdown rendering in assistant messages (headers, lists, bold, italic, code) - Better message spacing and visual distinction (border-left accents) - Styled markdown elements matching dark theme - Improved empty state and quick action button sizing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,88 @@
|
||||
messages = [];
|
||||
}
|
||||
|
||||
function formatMarkdown(text: string): string {
|
||||
// Split into lines for block-level processing
|
||||
const lines = text.split('\n');
|
||||
const result: string[] = [];
|
||||
let inList = false;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i];
|
||||
|
||||
// Headers
|
||||
if (line.startsWith('### ')) {
|
||||
if (inList) { result.push('</ul>'); inList = false; }
|
||||
const content = applyInlineFormatting(line.slice(4));
|
||||
result.push(`<h4>${content}</h4>`);
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('## ')) {
|
||||
if (inList) { result.push('</ul>'); inList = false; }
|
||||
const content = applyInlineFormatting(line.slice(3));
|
||||
result.push(`<h3>${content}</h3>`);
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('# ')) {
|
||||
if (inList) { result.push('</ul>'); inList = false; }
|
||||
const content = applyInlineFormatting(line.slice(2));
|
||||
result.push(`<h2>${content}</h2>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// List items (- or *)
|
||||
if (/^[\-\*] /.test(line)) {
|
||||
if (!inList) { result.push('<ul>'); inList = true; }
|
||||
const content = applyInlineFormatting(line.slice(2));
|
||||
result.push(`<li>${content}</li>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Numbered list items
|
||||
if (/^\d+\.\s/.test(line)) {
|
||||
if (!inList) { result.push('<ol>'); inList = true; }
|
||||
const content = applyInlineFormatting(line.replace(/^\d+\.\s/, ''));
|
||||
result.push(`<li>${content}</li>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Non-list line: close any open list
|
||||
if (inList) {
|
||||
// Check if previous list was ordered or unordered
|
||||
const lastOpen = result.findLast(r => r === '<ul>' || r === '<ol>');
|
||||
result.push(lastOpen === '<ol>' ? '</ol>' : '</ul>');
|
||||
inList = false;
|
||||
}
|
||||
|
||||
// Empty line = paragraph break
|
||||
if (line.trim() === '') {
|
||||
result.push('<br>');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular text line
|
||||
result.push(applyInlineFormatting(line));
|
||||
}
|
||||
|
||||
// Close any trailing open list
|
||||
if (inList) {
|
||||
const lastOpen = result.findLast(r => r === '<ul>' || r === '<ol>');
|
||||
result.push(lastOpen === '<ol>' ? '</ol>' : '</ul>');
|
||||
}
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
|
||||
function applyInlineFormatting(text: string): string {
|
||||
// Code blocks (backtick) — process first to avoid conflicts
|
||||
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
// Bold (**text**)
|
||||
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
// Italic (*text*) — only single asterisks not already consumed by bold
|
||||
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||
return text;
|
||||
}
|
||||
|
||||
// Quick action buttons
|
||||
async function summarize() {
|
||||
inputText = 'Please summarize this transcript in bullet points.';
|
||||
@@ -122,7 +204,11 @@
|
||||
{:else}
|
||||
{#each messages as msg}
|
||||
<div class="message {msg.role}">
|
||||
<div class="message-content">{msg.content}</div>
|
||||
{#if msg.role === 'assistant'}
|
||||
<div class="message-content">{@html formatMarkdown(msg.content)}</div>
|
||||
{:else}
|
||||
<div class="message-content">{msg.content}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if isLoading}
|
||||
@@ -192,47 +278,101 @@
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 0.8rem;
|
||||
padding: 1rem 0;
|
||||
color: #888;
|
||||
font-size: 0.85rem;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
.empty-state p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
margin-top: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.quick-btn {
|
||||
background: rgba(233, 69, 96, 0.15);
|
||||
border: 1px solid rgba(233, 69, 96, 0.3);
|
||||
color: #e94560;
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
padding: 0.45rem 0.85rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.quick-btn:hover {
|
||||
background: rgba(233, 69, 96, 0.25);
|
||||
}
|
||||
.message {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.message.user {
|
||||
background: rgba(233, 69, 96, 0.15);
|
||||
margin-left: 1rem;
|
||||
border-left: 3px solid rgba(233, 69, 96, 0.4);
|
||||
}
|
||||
.message.assistant {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
margin-right: 1rem;
|
||||
border-left: 3px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.message.loading {
|
||||
opacity: 0.6;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Markdown styles inside assistant messages */
|
||||
.message.assistant :global(h2) {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0.6rem 0 0.3rem;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
.message.assistant :global(h3) {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
margin: 0.5rem 0 0.25rem;
|
||||
color: #e8e8e8;
|
||||
}
|
||||
.message.assistant :global(h4) {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin: 0.4rem 0 0.2rem;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.message.assistant :global(strong) {
|
||||
color: #f0f0f0;
|
||||
font-weight: 600;
|
||||
}
|
||||
.message.assistant :global(em) {
|
||||
color: #ccc;
|
||||
font-style: italic;
|
||||
}
|
||||
.message.assistant :global(code) {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
color: #e94560;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.75rem;
|
||||
font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
||||
}
|
||||
.message.assistant :global(ul),
|
||||
.message.assistant :global(ol) {
|
||||
margin: 0.35rem 0;
|
||||
padding-left: 1.3rem;
|
||||
}
|
||||
.message.assistant :global(li) {
|
||||
margin-bottom: 0.25rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.message.assistant :global(br) {
|
||||
display: block;
|
||||
content: '';
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
.chat-input {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
Reference in New Issue
Block a user