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::get_active_session,
|
||||
session::set_claude_session_id,
|
||||
session::update_session_stats,
|
||||
// Messages
|
||||
db::save_message,
|
||||
db::load_messages,
|
||||
|
|
|
|||
|
|
@ -153,3 +153,30 @@ pub async fn set_claude_session_id(
|
|||
|
||||
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 });
|
||||
|
||||
// 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 {
|
||||
try {
|
||||
return marked.parse(text) as string;
|
||||
|
|
@ -831,7 +848,21 @@
|
|||
</div>
|
||||
{:else if message.role === 'assistant'}
|
||||
{#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}
|
||||
<span class="typing">
|
||||
<span class="dot"></span>
|
||||
|
|
@ -1270,6 +1301,29 @@
|
|||
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 */
|
||||
.message-content :global(p) {
|
||||
margin: 0.3em 0;
|
||||
|
|
|
|||
|
|
@ -126,7 +126,8 @@
|
|||
|
||||
function formatCost(usd: number): string {
|
||||
if (usd === 0) return '';
|
||||
return `$${usd.toFixed(3)}`;
|
||||
if (usd < 0.01) return `${(usd * 100).toFixed(1)}¢`;
|
||||
return `${usd.toFixed(2).replace('.', ',')}$`;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -381,6 +381,19 @@ export async function initEventListeners(): Promise<void> {
|
|||
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 {
|
||||
if (usd === 0) return '$0';
|
||||
if (usd < 0.01) return `$${usd.toFixed(4)}`;
|
||||
return `$${usd.toFixed(2)}`;
|
||||
if (usd === 0) return '0$';
|
||||
if (usd < 0.01) return `${(usd * 100).toFixed(1)}¢`;
|
||||
return `${usd.toFixed(2).replace('.', ',')}$`;
|
||||
}
|
||||
|
||||
function formatTokens(n: number): string {
|
||||
|
|
|
|||
Loading…
Reference in a new issue