2025-09-16 16:14:37 +00:00
|
|
|
<script lang="ts">
|
2025-12-04 23:04:20 +00:00
|
|
|
import ThoughtIndicator from "./ThoughtIndicator.svelte";
|
2025-09-28 22:31:35 +00:00
|
|
|
import StreamingIndicator from "./StreamingIndicator.svelte";
|
2026-07-11 13:04:19 +00:00
|
|
|
import UserMessage from "./UserMessage.svelte";
|
|
|
|
|
import AssistantMessage from "./AssistantMessage.svelte";
|
2025-10-02 08:34:54 +00:00
|
|
|
import { Greeting } from "Enums/Greeting";
|
2025-10-04 12:10:22 +00:00
|
|
|
import { Role } from "Enums/Role";
|
|
|
|
|
import type { ConversationContent } from "Conversations/ConversationContent";
|
2025-10-11 11:28:42 +00:00
|
|
|
import { tick } from "svelte";
|
2026-05-25 18:42:30 +00:00
|
|
|
import { setIcon } from "obsidian";
|
|
|
|
|
import { fade } from "svelte/transition";
|
2026-07-06 13:22:51 +00:00
|
|
|
import GraphAnimation from "./GraphAnimation.svelte";
|
2025-09-16 16:14:37 +00:00
|
|
|
|
2025-10-04 12:10:22 +00:00
|
|
|
export let messages: ConversationContent[] = [];
|
2025-10-07 21:27:51 +00:00
|
|
|
export let currentThought: string | null = null;
|
2025-10-06 23:57:08 +00:00
|
|
|
export let isSubmitting: boolean = false;
|
2025-09-28 16:40:05 +00:00
|
|
|
export let chatContainer: HTMLDivElement;
|
2025-10-02 19:52:43 +00:00
|
|
|
|
2025-10-24 16:25:19 +00:00
|
|
|
export function resetChatArea() {
|
|
|
|
|
chatContainer.scroll({ top: 0, behavior: "instant" });
|
2026-06-27 13:44:55 +00:00
|
|
|
tick().then(updateScrolledState);
|
2025-10-24 16:25:19 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 21:57:25 +00:00
|
|
|
export async function updateChatAreaLayout(behavior: ScrollBehavior | undefined = undefined) {
|
2026-01-30 19:36:52 +00:00
|
|
|
await tick();
|
2025-10-24 16:25:19 +00:00
|
|
|
|
2026-01-30 19:36:52 +00:00
|
|
|
requestAnimationFrame(() => {
|
2026-07-10 21:57:25 +00:00
|
|
|
if (behavior) {
|
|
|
|
|
scrollToLatestTurn(behavior);
|
|
|
|
|
}
|
|
|
|
|
tick().then(updateScrolledState);
|
2026-01-30 19:36:52 +00:00
|
|
|
});
|
2026-01-29 17:01:50 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 21:57:25 +00:00
|
|
|
function scrollToLatestTurn(behavior: ScrollBehavior) {
|
|
|
|
|
const latestTurn = chatContainer.querySelector<HTMLElement>(".message-group.latest");
|
|
|
|
|
if (!latestTurn) {
|
2026-01-29 17:01:50 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2026-07-10 21:57:25 +00:00
|
|
|
const paddingTop = parseFloat(getComputedStyle(chatContainer).paddingTop) || 0;
|
|
|
|
|
chatContainer.scroll({ top: latestTurn.offsetTop - paddingTop, behavior });
|
2025-10-11 23:33:04 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 21:57:25 +00:00
|
|
|
function scrollToBottom(behavior: ScrollBehavior) {
|
|
|
|
|
chatContainer.scroll({ top: chatContainer.scrollHeight, behavior });
|
2026-04-19 22:53:14 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-25 18:42:30 +00:00
|
|
|
function updateScrolledState() {
|
2026-06-27 13:44:55 +00:00
|
|
|
if (!chatContainer) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const isScrollable = chatContainer.scrollHeight > chatContainer.clientHeight;
|
|
|
|
|
scrolledToBottom = !isScrollable || Math.abs(chatContainer.scrollHeight - chatContainer.scrollTop - chatContainer.clientHeight) < 100;
|
2026-05-25 18:42:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let scrolledToBottom: boolean = true;
|
|
|
|
|
|
|
|
|
|
let scrollToBottomButton: HTMLButtonElement;
|
2025-10-11 11:28:42 +00:00
|
|
|
|
2026-07-10 21:57:25 +00:00
|
|
|
type Turn = { id: number, messages: ConversationContent[] };
|
|
|
|
|
|
|
|
|
|
$: turns = groupTurns(messages);
|
|
|
|
|
|
|
|
|
|
// A turn starts at each visible user message; hidden contents (tool
|
|
|
|
|
// responses, planning notices, attachment stubs) stay attached to the
|
|
|
|
|
// turn they belong to instead of starting phantom turns
|
|
|
|
|
function groupTurns(messages: ConversationContent[]): Turn[] {
|
|
|
|
|
const turns: Turn[] = [];
|
|
|
|
|
for (const message of messages) {
|
|
|
|
|
if ((message.role === Role.User && message.shouldDisplayContent) || turns.length === 0) {
|
|
|
|
|
turns.push({ id: message.id, messages: [] });
|
|
|
|
|
}
|
|
|
|
|
turns[turns.length - 1].messages.push(message);
|
|
|
|
|
}
|
|
|
|
|
return turns;
|
|
|
|
|
}
|
2025-10-05 17:43:48 +00:00
|
|
|
|
2025-10-02 08:34:54 +00:00
|
|
|
function getGreetingByTime(): string {
|
|
|
|
|
const hour = new Date().getHours();
|
|
|
|
|
|
|
|
|
|
// Morning: 5am - 11:59am
|
|
|
|
|
if (hour >= 5 && hour < 12) {
|
|
|
|
|
return Greeting.Morning;
|
|
|
|
|
}
|
|
|
|
|
// Midday: 12pm - 4:59pm
|
|
|
|
|
else if (hour >= 12 && hour < 17) {
|
|
|
|
|
return Greeting.Midday;
|
|
|
|
|
}
|
|
|
|
|
// Evening: 5pm - 8:59pm
|
|
|
|
|
else if (hour >= 17 && hour < 21) {
|
|
|
|
|
return Greeting.Evening;
|
|
|
|
|
}
|
|
|
|
|
// Night: 9pm - 4:59am
|
|
|
|
|
else {
|
|
|
|
|
return Greeting.Night;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 18:42:30 +00:00
|
|
|
$: if (scrollToBottomButton) {
|
|
|
|
|
setIcon(scrollToBottomButton, "arrow-down");
|
|
|
|
|
}
|
2025-09-16 16:14:37 +00:00
|
|
|
</script>
|
|
|
|
|
|
2025-12-20 11:20:43 +00:00
|
|
|
<div class="chat-area-wrapper">
|
|
|
|
|
{#if messages.length > 0}
|
2025-12-22 20:02:02 +00:00
|
|
|
<div class="top-fade"></div>
|
2025-12-20 11:20:43 +00:00
|
|
|
{/if}
|
2026-05-25 18:42:30 +00:00
|
|
|
<div class="chat-area" bind:this={chatContainer} on:scroll={updateScrolledState}>
|
2026-07-10 21:57:25 +00:00
|
|
|
{#each turns as turn, turnIndex (turn.id)}
|
|
|
|
|
{@const isLatestTurn = turnIndex === turns.length - 1}
|
|
|
|
|
<div class="message-group" class:latest={isLatestTurn}>
|
|
|
|
|
{#each turn.messages as message (message.id)}
|
|
|
|
|
{@const content = message.getDisplayContent()}
|
|
|
|
|
{#if message.shouldDisplayContent && content.trim() !== ""}
|
|
|
|
|
{#if message.role === Role.User}
|
2026-07-11 13:04:19 +00:00
|
|
|
<UserMessage {message} />
|
2026-07-10 21:57:25 +00:00
|
|
|
{:else}
|
2026-07-11 13:04:19 +00:00
|
|
|
<AssistantMessage {message} {isSubmitting} />
|
2026-07-10 21:57:25 +00:00
|
|
|
{/if}
|
|
|
|
|
{/if}
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
|
|
{#if isLatestTurn}
|
|
|
|
|
<ThoughtIndicator thought={currentThought}/>
|
|
|
|
|
{#if isSubmitting}
|
|
|
|
|
<div transition:fade={{ duration: 300 }}>
|
|
|
|
|
<StreamingIndicator/>
|
2025-10-06 23:57:08 +00:00
|
|
|
</div>
|
2026-07-10 21:57:25 +00:00
|
|
|
{/if}
|
2025-12-20 11:20:43 +00:00
|
|
|
{/if}
|
2026-07-06 18:05:19 +00:00
|
|
|
</div>
|
2026-07-10 21:57:25 +00:00
|
|
|
{/each}
|
2025-12-04 23:04:20 +00:00
|
|
|
|
2025-12-20 11:20:43 +00:00
|
|
|
{#if messages.length === 0}
|
2026-07-06 13:22:51 +00:00
|
|
|
<div class="conversation-empty-container">
|
|
|
|
|
<div class="conversation-empty-animation">
|
|
|
|
|
<GraphAnimation/>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="conversation-empty-greeting">
|
|
|
|
|
<div class="conversation-empty-greeting-backdrop"></div>
|
|
|
|
|
<div class="conversation-empty-greeting-label">{getGreetingByTime()}</div>
|
|
|
|
|
</div>
|
2025-12-20 11:20:43 +00:00
|
|
|
</div>
|
|
|
|
|
{/if}
|
2026-05-25 18:42:30 +00:00
|
|
|
|
2025-12-20 11:20:43 +00:00
|
|
|
</div>
|
2026-05-25 18:42:30 +00:00
|
|
|
|
2025-12-20 11:20:43 +00:00
|
|
|
{#if messages.length > 0}
|
2025-12-22 20:02:02 +00:00
|
|
|
<div class="bottom-fade"></div>
|
2025-09-16 16:14:37 +00:00
|
|
|
{/if}
|
2026-05-25 18:42:30 +00:00
|
|
|
|
|
|
|
|
{#if !scrolledToBottom}
|
|
|
|
|
<div class="scroll-to-bottom-container" transition:fade>
|
|
|
|
|
<button
|
|
|
|
|
id="scroll-to-bottom-button"
|
|
|
|
|
bind:this={scrollToBottomButton}
|
2026-07-10 21:57:25 +00:00
|
|
|
on:click={() => scrollToBottom("smooth")}
|
2026-05-25 18:42:30 +00:00
|
|
|
aria-label="Scroll to bottom">
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
2025-09-16 16:14:37 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<style>
|
2026-05-25 18:42:30 +00:00
|
|
|
.scroll-to-bottom-container {
|
|
|
|
|
background-color: color-mix(in srgb, var(--background-primary) 70%, transparent);
|
|
|
|
|
border-radius: var(--radius-l);
|
|
|
|
|
padding: var(--size-4-2);
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: var(--size-4-2);
|
|
|
|
|
right: 0px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#scroll-to-bottom-button {
|
|
|
|
|
background-color: var(--interactive-accent);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-20 11:20:43 +00:00
|
|
|
.chat-area-wrapper {
|
|
|
|
|
position: relative;
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-20 01:34:41 +00:00
|
|
|
.top-fade {
|
|
|
|
|
position: absolute;
|
2025-12-20 11:20:43 +00:00
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
2025-12-20 01:34:41 +00:00
|
|
|
height: var(--size-4-4);
|
|
|
|
|
background-image: linear-gradient(to bottom, var(--background-secondary), transparent);
|
2025-12-20 11:20:43 +00:00
|
|
|
z-index: 10;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bottom-fade {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
height: var(--size-4-4);
|
|
|
|
|
background-image: linear-gradient(to top, var(--background-secondary), transparent);
|
|
|
|
|
z-index: 10;
|
|
|
|
|
pointer-events: none;
|
2025-12-20 01:34:41 +00:00
|
|
|
}
|
|
|
|
|
|
2025-09-16 16:14:37 +00:00
|
|
|
.chat-area {
|
2025-12-20 11:20:43 +00:00
|
|
|
position: relative;
|
2025-09-16 16:14:37 +00:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100%;
|
2025-10-01 21:12:39 +00:00
|
|
|
overflow: auto;
|
2025-12-20 01:34:41 +00:00
|
|
|
padding: var(--size-4-4) var(--size-4-3) var(--size-4-3) var(--size-4-3);
|
2025-10-01 21:12:39 +00:00
|
|
|
gap: var(--size-4-2);
|
|
|
|
|
scroll-behavior: smooth;
|
2025-09-16 16:14:37 +00:00
|
|
|
}
|
2025-09-28 23:09:10 +00:00
|
|
|
|
|
|
|
|
.chat-area::-webkit-scrollbar {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
2025-09-29 10:09:22 +00:00
|
|
|
|
2026-07-10 21:57:25 +00:00
|
|
|
.message-group {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
gap: var(--size-4-2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-group.latest {
|
2026-07-11 12:49:44 +00:00
|
|
|
margin-top: var(--size-4-2);
|
2026-07-10 21:57:25 +00:00
|
|
|
min-height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 13:22:51 +00:00
|
|
|
.conversation-empty-container {
|
|
|
|
|
display: grid;
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.conversation-empty-animation {
|
|
|
|
|
grid-row: 1;
|
|
|
|
|
grid-column: 1;
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.conversation-empty-greeting-backdrop {
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: -48px;
|
|
|
|
|
background: radial-gradient(
|
|
|
|
|
ellipse closest-side,
|
|
|
|
|
var(--background-secondary) 0%,
|
|
|
|
|
color-mix(in srgb, var(--background-secondary) 80%, transparent) 65%,
|
|
|
|
|
transparent 100%
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.conversation-empty-greeting {
|
|
|
|
|
grid-row: 1;
|
|
|
|
|
grid-column: 1;
|
|
|
|
|
position: relative;
|
2025-10-05 12:06:05 +00:00
|
|
|
margin: auto;
|
2025-09-16 16:14:37 +00:00
|
|
|
font-style: italic;
|
2025-10-08 21:06:25 +00:00
|
|
|
font-size: var(--font-ui-medium);
|
2025-09-16 16:14:37 +00:00
|
|
|
color: var(--text-muted);
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
2025-09-26 19:48:05 +00:00
|
|
|
|
2026-07-06 13:22:51 +00:00
|
|
|
.conversation-empty-greeting-label {
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
padding: var(--size-2-2);
|
|
|
|
|
}
|
2025-09-16 16:14:37 +00:00
|
|
|
</style>
|