feat: collapsible messages, German cost format, stats persistence [appimage]
Some checks failed
Build AppImage / build (push) Has been cancelled
Some checks failed
Build AppImage / build (push) Has been cancelled
- Long messages (>25 lines) auto-collapse with expand/collapse button - Cost display uses German format: "16,23$" instead of "$16.230" - Session stats (tokens, cost, count) persist to DB after each response via new update_session_stats command — survives app restart - Small costs shown as cents (e.g. "3,2¢") Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
50d46dca79
commit
8a7e0d87f3
6 changed files with 101 additions and 5 deletions
|
|
@ -77,6 +77,7 @@ pub fn run() {
|
||||||
session::resume_session,
|
session::resume_session,
|
||||||
session::get_active_session,
|
session::get_active_session,
|
||||||
session::set_claude_session_id,
|
session::set_claude_session_id,
|
||||||
|
session::update_session_stats,
|
||||||
// Messages
|
// Messages
|
||||||
db::save_message,
|
db::save_message,
|
||||||
db::load_messages,
|
db::load_messages,
|
||||||
|
|
|
||||||
|
|
@ -153,3 +153,30 @@ pub async fn set_claude_session_id(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Aktualisiert nur die Statistiken einer Session (Token, Kosten, Message-Count)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_session_stats(
|
||||||
|
app: AppHandle,
|
||||||
|
session_id: String,
|
||||||
|
token_input: i64,
|
||||||
|
token_output: i64,
|
||||||
|
cost_usd: f64,
|
||||||
|
message_count: i32,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||||
|
let db = state.lock().unwrap();
|
||||||
|
db.conn.execute(
|
||||||
|
"UPDATE sessions SET token_input = ?1, token_output = ?2, cost_usd = ?3,
|
||||||
|
message_count = ?4, updated_at = ?5 WHERE id = ?6",
|
||||||
|
rusqlite::params![
|
||||||
|
token_input,
|
||||||
|
token_output,
|
||||||
|
cost_usd,
|
||||||
|
message_count,
|
||||||
|
chrono::Utc::now().to_rfc3339(),
|
||||||
|
session_id,
|
||||||
|
],
|
||||||
|
).map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,23 @@
|
||||||
|
|
||||||
marked.setOptions({ breaks: true, gfm: true, renderer });
|
marked.setOptions({ breaks: true, gfm: true, renderer });
|
||||||
|
|
||||||
|
// Collapse: Nachrichten mit > 25 Zeilen werden eingeklappt
|
||||||
|
const COLLAPSE_LINES = 25;
|
||||||
|
let expandedMessages = new Set<string>();
|
||||||
|
|
||||||
|
function toggleExpand(msgId: string) {
|
||||||
|
if (expandedMessages.has(msgId)) {
|
||||||
|
expandedMessages.delete(msgId);
|
||||||
|
} else {
|
||||||
|
expandedMessages.add(msgId);
|
||||||
|
}
|
||||||
|
expandedMessages = expandedMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldCollapse(content: string): boolean {
|
||||||
|
return content.split('\n').length > COLLAPSE_LINES;
|
||||||
|
}
|
||||||
|
|
||||||
function renderMarkdown(text: string): string {
|
function renderMarkdown(text: string): string {
|
||||||
try {
|
try {
|
||||||
return marked.parse(text) as string;
|
return marked.parse(text) as string;
|
||||||
|
|
@ -831,7 +848,21 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if message.role === 'assistant'}
|
{:else if message.role === 'assistant'}
|
||||||
{#if message.content}
|
{#if message.content}
|
||||||
{@html renderMarkdown(message.content)}
|
{#if shouldCollapse(message.content) && !expandedMessages.has(message.id)}
|
||||||
|
<div class="msg-collapsed">
|
||||||
|
{@html renderMarkdown(message.content.split('\n').slice(0, COLLAPSE_LINES).join('\n') + '\n...')}
|
||||||
|
</div>
|
||||||
|
<button class="expand-btn" onclick={() => toggleExpand(message.id)}>
|
||||||
|
▼ Mehr anzeigen ({message.content.split('\n').length} Zeilen)
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
{@html renderMarkdown(message.content)}
|
||||||
|
{#if shouldCollapse(message.content)}
|
||||||
|
<button class="expand-btn" onclick={() => toggleExpand(message.id)}>
|
||||||
|
▲ Einklappen
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
{:else if $isProcessing}
|
{:else if $isProcessing}
|
||||||
<span class="typing">
|
<span class="typing">
|
||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
|
|
@ -1270,6 +1301,29 @@
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.msg-collapsed {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.35rem;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
background: var(--bg-tertiary, #2a2a2a);
|
||||||
|
border: 1px solid var(--border, #3a3a3a);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--accent, #6aa3ff);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn:hover {
|
||||||
|
background: var(--bg-hover, #333);
|
||||||
|
}
|
||||||
|
|
||||||
/* Markdown-Styles innerhalb von Nachrichten */
|
/* Markdown-Styles innerhalb von Nachrichten */
|
||||||
.message-content :global(p) {
|
.message-content :global(p) {
|
||||||
margin: 0.3em 0;
|
margin: 0.3em 0;
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,8 @@
|
||||||
|
|
||||||
function formatCost(usd: number): string {
|
function formatCost(usd: number): string {
|
||||||
if (usd === 0) return '';
|
if (usd === 0) return '';
|
||||||
return `$${usd.toFixed(3)}`;
|
if (usd < 0.01) return `${(usd * 100).toFixed(1)}¢`;
|
||||||
|
return `${usd.toFixed(2).replace('.', ',')}$`;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -381,6 +381,19 @@ export async function initEventListeners(): Promise<void> {
|
||||||
outputTokens: tokens.output || 0,
|
outputTokens: tokens.output || 0,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session-Stats in DB persistieren (überlebt App-Neustart)
|
||||||
|
const appSessionId = get(currentSessionId);
|
||||||
|
if (appSessionId) {
|
||||||
|
const stats = get(sessionStats);
|
||||||
|
invoke('update_session_stats', {
|
||||||
|
sessionId: appSessionId,
|
||||||
|
tokenInput: stats.totalTokensIn,
|
||||||
|
tokenOutput: stats.totalTokensOut,
|
||||||
|
costUsd: stats.totalCost,
|
||||||
|
messageCount: stats.messageCount,
|
||||||
|
}).catch((err: unknown) => console.warn('Session-Stats speichern fehlgeschlagen:', err));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -136,9 +136,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCost(usd: number): string {
|
function formatCost(usd: number): string {
|
||||||
if (usd === 0) return '$0';
|
if (usd === 0) return '0$';
|
||||||
if (usd < 0.01) return `$${usd.toFixed(4)}`;
|
if (usd < 0.01) return `${(usd * 100).toFixed(1)}¢`;
|
||||||
return `$${usd.toFixed(2)}`;
|
return `${usd.toFixed(2).replace('.', ',')}$`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTokens(n: number): string {
|
function formatTokens(n: number): string {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue