floating chat
This commit is contained in:
@@ -104,12 +104,12 @@
|
|||||||
<main class="app-main">
|
<main class="app-main">
|
||||||
<router-outlet />
|
<router-outlet />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<app-assistant-chat class="assistant-sidebar" />
|
|
||||||
</div>
|
</div>
|
||||||
</mat-sidenav-content>
|
</mat-sidenav-content>
|
||||||
</mat-sidenav-container>
|
</mat-sidenav-container>
|
||||||
|
|
||||||
|
<app-assistant-chat />
|
||||||
|
|
||||||
<nav class="bottom-nav" aria-label="Mobile Hauptnavigation">
|
<nav class="bottom-nav" aria-label="Mobile Hauptnavigation">
|
||||||
<a
|
<a
|
||||||
class="bottom-nav-link"
|
class="bottom-nav-link"
|
||||||
|
|||||||
@@ -125,10 +125,6 @@
|
|||||||
min-height: calc(100dvh - 56px);
|
min-height: calc(100dvh - 56px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-sidebar {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shell .app-main {
|
.shell .app-main {
|
||||||
padding-bottom: calc(76px + env(safe-area-inset-bottom));
|
padding-bottom: calc(76px + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
@@ -232,16 +228,3 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1041px) {
|
|
||||||
.shell-workspace {
|
|
||||||
grid-template-columns: minmax(0, 1fr) 360px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.assistant-sidebar {
|
|
||||||
position: sticky;
|
|
||||||
top: 64px;
|
|
||||||
min-height: calc(100dvh - 64px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,70 +1,95 @@
|
|||||||
<section class="assistant-shell" aria-label="Listify-Assistent">
|
<div class="assistant-floating">
|
||||||
<header class="assistant-header">
|
@if (open()) {
|
||||||
<div>
|
<section class="assistant-shell" aria-label="Listify-Assistent">
|
||||||
<h2>Assistent</h2>
|
<header class="assistant-header">
|
||||||
<p>{{ onlineStatus.online() ? 'Listen planen und erstellen' : 'Nur online verfuegbar' }}</p>
|
<div>
|
||||||
</div>
|
<h2>Assistent</h2>
|
||||||
<mat-icon aria-hidden="true">{{ onlineStatus.online() ? 'auto_awesome' : 'cloud_off' }}</mat-icon>
|
<p>{{ onlineStatus.online() ? 'Listen planen und erstellen' : 'Nur online verfuegbar' }}</p>
|
||||||
</header>
|
</div>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
type="button"
|
||||||
|
aria-label="Assistent schliessen"
|
||||||
|
(click)="close()"
|
||||||
|
>
|
||||||
|
<mat-icon aria-hidden="true">close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div class="message-list" aria-live="polite">
|
<div class="message-list" aria-live="polite">
|
||||||
@for (message of messages(); track $index) {
|
@for (message of messages(); track $index) {
|
||||||
<article class="message" [class.user-message]="message.role === 'user'">
|
<article class="message" [class.user-message]="message.role === 'user'">
|
||||||
<div
|
<div
|
||||||
class="message-content"
|
class="message-content"
|
||||||
[innerHTML]="formatMessage(message.content)"
|
[innerHTML]="formatMessage(message.content)"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
@if (message.actions?.length) {
|
@if (message.actions?.length) {
|
||||||
<div class="action-list">
|
<div class="action-list">
|
||||||
@for (action of message.actions; track action.type + action.listId) {
|
@for (action of message.actions; track action.type + action.listId) {
|
||||||
<a mat-stroked-button [routerLink]="['/lists', action.listId]">
|
<a mat-stroked-button [routerLink]="['/lists', action.listId]">
|
||||||
<mat-icon aria-hidden="true">open_in_new</mat-icon>
|
<mat-icon aria-hidden="true">open_in_new</mat-icon>
|
||||||
{{ actionLabel(action) }}
|
{{ actionLabel(action) }}
|
||||||
</a>
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (sending()) {
|
||||||
|
<div class="assistant-loading">
|
||||||
|
<mat-progress-spinner mode="indeterminate" diameter="22" />
|
||||||
|
<span>Antwort wird erstellt</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</article>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (sending()) {
|
|
||||||
<div class="assistant-loading">
|
|
||||||
<mat-progress-spinner mode="indeterminate" diameter="22" />
|
|
||||||
<span>Antwort wird erstellt</span>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (errorMessage()) {
|
@if (errorMessage()) {
|
||||||
<p class="assistant-error">
|
<p class="assistant-error">
|
||||||
<mat-icon aria-hidden="true">error</mat-icon>
|
<mat-icon aria-hidden="true">error</mat-icon>
|
||||||
{{ errorMessage() }}
|
{{ errorMessage() }}
|
||||||
</p>
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="composer">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Nachricht</mat-label>
|
||||||
|
<textarea
|
||||||
|
matInput
|
||||||
|
rows="3"
|
||||||
|
[value]="draft()"
|
||||||
|
[disabled]="!onlineStatus.online()"
|
||||||
|
(input)="draft.set($any($event.target).value)"
|
||||||
|
(keydown.enter)="handleEnter($event)"
|
||||||
|
></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<button
|
||||||
|
mat-flat-button
|
||||||
|
type="button"
|
||||||
|
[disabled]="!canSend()"
|
||||||
|
(click)="send()"
|
||||||
|
aria-label="Nachricht senden"
|
||||||
|
>
|
||||||
|
<mat-icon aria-hidden="true">send</mat-icon>
|
||||||
|
Senden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="composer">
|
<button
|
||||||
<mat-form-field appearance="outline">
|
mat-fab
|
||||||
<mat-label>Nachricht</mat-label>
|
type="button"
|
||||||
<textarea
|
class="assistant-launcher"
|
||||||
matInput
|
[class.offline]="!onlineStatus.online()"
|
||||||
rows="3"
|
[attr.aria-expanded]="open()"
|
||||||
[value]="draft()"
|
[attr.aria-label]="open() ? 'Assistent schliessen' : 'Assistent oeffnen'"
|
||||||
[disabled]="!onlineStatus.online()"
|
(click)="toggleOpen()"
|
||||||
(input)="draft.set($any($event.target).value)"
|
>
|
||||||
(keydown.enter)="handleEnter($event)"
|
<mat-icon aria-hidden="true">
|
||||||
></textarea>
|
{{ open() ? 'close' : (onlineStatus.online() ? 'auto_awesome' : 'cloud_off') }}
|
||||||
</mat-form-field>
|
</mat-icon>
|
||||||
|
</button>
|
||||||
<button
|
</div>
|
||||||
mat-flat-button
|
|
||||||
type="button"
|
|
||||||
[disabled]="!canSend()"
|
|
||||||
(click)="send()"
|
|
||||||
aria-label="Nachricht senden"
|
|
||||||
>
|
|
||||||
<mat-icon aria-hidden="true">send</mat-icon>
|
|
||||||
Senden
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|||||||
@@ -1,18 +1,32 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: contents;
|
||||||
min-width: 0;
|
}
|
||||||
|
|
||||||
|
.assistant-floating {
|
||||||
|
position: fixed;
|
||||||
|
right: 1rem;
|
||||||
|
bottom: calc(76px + env(safe-area-inset-bottom));
|
||||||
|
z-index: 35;
|
||||||
|
display: grid;
|
||||||
|
justify-items: end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-shell {
|
.assistant-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(220px, 1fr) auto auto;
|
grid-template-rows: auto minmax(220px, 1fr) auto auto;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
width: 100%;
|
width: min(360px, calc(100vw - 2rem));
|
||||||
min-height: 420px;
|
height: min(640px, calc(100dvh - 166px));
|
||||||
max-height: calc(100dvh - 88px);
|
min-height: 360px;
|
||||||
padding: 0.85rem;
|
padding: 0.85rem;
|
||||||
border-left: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent);
|
border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent);
|
||||||
background: color-mix(in srgb, var(--mat-sys-surface) 94%, transparent);
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--mat-sys-surface) 96%, transparent);
|
||||||
|
box-shadow: 0 18px 48px color-mix(in srgb, var(--mat-sys-shadow) 20%, transparent);
|
||||||
|
pointer-events: auto;
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-header {
|
.assistant-header {
|
||||||
@@ -44,6 +58,10 @@
|
|||||||
color: var(--mat-sys-primary);
|
color: var(--mat-sys-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assistant-header button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.message-list {
|
.message-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -158,10 +176,34 @@
|
|||||||
justify-self: end;
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1040px) {
|
.assistant-launcher {
|
||||||
|
pointer-events: auto;
|
||||||
|
box-shadow: 0 12px 28px color-mix(in srgb, var(--mat-sys-shadow) 24%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-launcher.offline {
|
||||||
|
--mdc-fab-container-color: var(--mat-sys-error-container);
|
||||||
|
--mat-fab-foreground-color: var(--mat-sys-on-error-container);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 801px) {
|
||||||
|
.assistant-floating {
|
||||||
|
bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.assistant-shell {
|
.assistant-shell {
|
||||||
max-height: none;
|
height: min(640px, calc(100dvh - 6rem));
|
||||||
border-top: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent);
|
}
|
||||||
border-left: 0;
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.assistant-floating {
|
||||||
|
right: 0.75rem;
|
||||||
|
left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-shell {
|
||||||
|
width: 100%;
|
||||||
|
height: min(620px, calc(100dvh - 154px));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export class AssistantChatComponent {
|
|||||||
protected readonly draft = signal('');
|
protected readonly draft = signal('');
|
||||||
protected readonly sending = signal(false);
|
protected readonly sending = signal(false);
|
||||||
protected readonly errorMessage = signal<string | null>(null);
|
protected readonly errorMessage = signal<string | null>(null);
|
||||||
|
protected readonly open = signal(false);
|
||||||
protected readonly canSend = computed(
|
protected readonly canSend = computed(
|
||||||
() =>
|
() =>
|
||||||
this.draft().trim().length > 0 &&
|
this.draft().trim().length > 0 &&
|
||||||
@@ -109,6 +110,14 @@ export class AssistantChatComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected toggleOpen(): void {
|
||||||
|
this.open.update((open) => !open);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected close(): void {
|
||||||
|
this.open.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
private resolveContext(): Observable<AssistantPageContext> {
|
private resolveContext(): Observable<AssistantPageContext> {
|
||||||
const route = this.router.url || '/';
|
const route = this.router.url || '/';
|
||||||
const path = route.split(/[?#]/)[0] || '/';
|
const path = route.split(/[?#]/)[0] || '/';
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export class ListsComponent implements OnInit {
|
|||||||
protected readonly errorMessage = signal<string | null>(null);
|
protected readonly errorMessage = signal<string | null>(null);
|
||||||
protected readonly searchTerm = signal('');
|
protected readonly searchTerm = signal('');
|
||||||
protected readonly kindFilter = signal<ListKindFilter>('all');
|
protected readonly kindFilter = signal<ListKindFilter>('all');
|
||||||
protected readonly statusFilter = signal<ListStatusFilter>('all');
|
protected readonly statusFilter = signal<ListStatusFilter>('open');
|
||||||
protected readonly sortOption = signal<ListSortOption>('updated-desc');
|
protected readonly sortOption = signal<ListSortOption>('updated-desc');
|
||||||
protected readonly hasLists = computed(() => this.lists().length > 0);
|
protected readonly hasLists = computed(() => this.lists().length > 0);
|
||||||
protected readonly visibleLists = computed(() => {
|
protected readonly visibleLists = computed(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user