@things-factory/ai-assistant 10.1.109 → 10.1.111
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/components/assistant-chat.ts +290 -18
- package/client/components/assistant-dock.ts +8 -3
- package/client/setup-assistant-dock.ts +9 -1
- package/client/utils/composer-frame.ts +49 -0
- package/client/utils/pasted-table.ts +2 -2
- package/dist-client/components/assistant-chat.d.ts +18 -0
- package/dist-client/components/assistant-chat.js +291 -18
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.js +9 -3
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/setup-assistant-dock.js +8 -1
- package/dist-client/setup-assistant-dock.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/composer-frame.d.ts +28 -0
- package/dist-client/utils/composer-frame.js +46 -0
- package/dist-client/utils/composer-frame.js.map +1 -0
- package/dist-client/utils/pasted-table.d.ts +1 -0
- package/dist-client/utils/pasted-table.js +1 -1
- package/dist-client/utils/pasted-table.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/test/assistant-dock.test.ts +28 -0
- package/test/composer-frame.test.ts +125 -0
- package/test/pasted-table.test.ts +1 -1
- package/translations/en.json +7 -1
- package/translations/ja.json +7 -1
- package/translations/ko.json +7 -1
- package/translations/ms.json +7 -1
- package/translations/zh.json +7 -1
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from './markdown.js'
|
|
35
35
|
import { acceptOf, chatAttachmentUrl, refuseAttachments, textCharsOf } from '../utils/attachment-policy.js'
|
|
36
36
|
import { pastedTableFile } from '../utils/pasted-table.js'
|
|
37
|
+
import { appendWords, composerHeight, composerKeyAction, sendShortcut, tableAsWords } from '../utils/composer-frame.js'
|
|
37
38
|
import { readSpreadsheet, spreadsheetKindOf, type SpreadsheetRefusal } from '../utils/spreadsheet-text.js'
|
|
38
39
|
import { entryReviewAddress, offersWorkspaceReview } from '../utils/entry-workspace.js'
|
|
39
40
|
import { buildAssistantChatInput, buildChatMutationInput } from './chat-input-builder.js'
|
|
@@ -141,6 +142,8 @@ export interface ChatAttachment {
|
|
|
141
142
|
type: string
|
|
142
143
|
url: string
|
|
143
144
|
base64?: string
|
|
145
|
+
/** A table pasted into the composer - its shape for the chip, its rows for the preview, its words to give back. */
|
|
146
|
+
table?: { rows: string[][]; columns: number; text?: string }
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
function formatFileSize(bytes: number): string {
|
|
@@ -605,6 +608,14 @@ export class OxAssistantChat extends LitElement {
|
|
|
605
608
|
@state()
|
|
606
609
|
private dragOver = false
|
|
607
610
|
|
|
611
|
+
/** The wide box (넓게 쓰기) covers the conversation inside the dock - shared-dock.md §10. */
|
|
612
|
+
@state()
|
|
613
|
+
private composerWide = false
|
|
614
|
+
|
|
615
|
+
/** The pasted table whose rows cover the conversation, or null. */
|
|
616
|
+
@state()
|
|
617
|
+
private previewAttachmentId: string | null = null
|
|
618
|
+
|
|
608
619
|
/*
|
|
609
620
|
* ── 왜 이미지만 받나 ──────────────────────────────────────────────────────
|
|
610
621
|
* `.json` · `.txt` · `.csv` 도 받고 있었다. 그 파일들은 `hostContext.attachments` 로 실려 갔고
|
|
@@ -640,7 +651,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
640
651
|
return String(i18next.t(`ai-assistant.text.${refusal.code}`, { ...params, defaultValue: refusal.code }))
|
|
641
652
|
}
|
|
642
653
|
|
|
643
|
-
private async onAttachFiles(files: FileList | File[] | null | undefined) {
|
|
654
|
+
private async onAttachFiles(files: FileList | File[] | null | undefined, table?: ChatAttachment['table']) {
|
|
644
655
|
if (!files || files.length === 0) return
|
|
645
656
|
let list = Array.from(files)
|
|
646
657
|
if (this.usesAttachmentPolicy && this.modeView?.attachments?.kinds?.includes('spreadsheet')) {
|
|
@@ -678,7 +689,8 @@ export class OxAssistantChat extends LitElement {
|
|
|
678
689
|
size: file.size,
|
|
679
690
|
type: file.type || 'application/octet-stream',
|
|
680
691
|
url,
|
|
681
|
-
base64
|
|
692
|
+
base64,
|
|
693
|
+
table
|
|
682
694
|
})
|
|
683
695
|
}
|
|
684
696
|
|
|
@@ -722,6 +734,39 @@ export class OxAssistantChat extends LitElement {
|
|
|
722
734
|
URL.revokeObjectURL(target.url)
|
|
723
735
|
}
|
|
724
736
|
this.attachments = this.attachments.filter(a => a.id !== id)
|
|
737
|
+
if (this.previewAttachmentId === id) this.previewAttachmentId = null
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** A pasted table goes back into the box as the words it was copied as - the conversion was not the person's choice. */
|
|
741
|
+
private tableToWords(id: string) {
|
|
742
|
+
const target = this.attachments.find(a => a.id === id)
|
|
743
|
+
if (!target?.table) return
|
|
744
|
+
this.input = appendWords(this.input, tableAsWords(target.table))
|
|
745
|
+
this.removeAttachment(id)
|
|
746
|
+
this.updateComplete.then(() => (this.renderRoot.querySelector('textarea') as HTMLTextAreaElement | null)?.focus())
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** The small box stands as tall as its text, up to 40% of the chat; the wide box fills what it covers. */
|
|
750
|
+
private fitComposer() {
|
|
751
|
+
const ta = this.renderRoot.querySelector('.input-row textarea') as HTMLTextAreaElement | null
|
|
752
|
+
if (!ta) return
|
|
753
|
+
if (this.composerWide) {
|
|
754
|
+
ta.style.height = ''
|
|
755
|
+
ta.style.overflowY = ''
|
|
756
|
+
return
|
|
757
|
+
}
|
|
758
|
+
ta.style.height = '0px'
|
|
759
|
+
const border = ta.offsetHeight - ta.clientHeight
|
|
760
|
+
const resting = parseFloat(getComputedStyle(ta).minHeight) || 0
|
|
761
|
+
const { height, scrolls } = composerHeight(ta.scrollHeight + border, this.clientHeight, resting)
|
|
762
|
+
ta.style.height = `${height}px`
|
|
763
|
+
ta.style.overflowY = scrolls ? 'auto' : 'hidden'
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
private setComposerWide(wide: boolean) {
|
|
767
|
+
this.composerWide = wide
|
|
768
|
+
if (wide) this.previewAttachmentId = null
|
|
769
|
+
this.updateComplete.then(() => (this.renderRoot.querySelector('.input-row textarea') as HTMLTextAreaElement | null)?.focus())
|
|
725
770
|
}
|
|
726
771
|
|
|
727
772
|
private onComposerPaste(e: ClipboardEvent) {
|
|
@@ -744,7 +789,8 @@ export class OxAssistantChat extends LitElement {
|
|
|
744
789
|
this.errorMessage = String(i18next.t(`ai-assistant.text.${pasted.refusal.code}`, { ...pasted.refusal, defaultValue: pasted.refusal.code }))
|
|
745
790
|
return
|
|
746
791
|
}
|
|
747
|
-
|
|
792
|
+
const words = e.clipboardData?.getData('text/plain')
|
|
793
|
+
this.onAttachFiles([new File([pasted.text], pasted.name, { type: 'text/csv' })], { ...pasted.table, text: words })
|
|
748
794
|
return
|
|
749
795
|
}
|
|
750
796
|
}
|
|
@@ -2363,6 +2409,128 @@ export class OxAssistantChat extends LitElement {
|
|
|
2363
2409
|
flex-direction: column;
|
|
2364
2410
|
transition: background 0.15s, border-color 0.15s;
|
|
2365
2411
|
}
|
|
2412
|
+
/* The wide box and a table's rows cover the conversation, inside the dock - the page beside it stays. */
|
|
2413
|
+
.messages.covered {
|
|
2414
|
+
flex: 0 0 0;
|
|
2415
|
+
padding: 0;
|
|
2416
|
+
overflow: hidden;
|
|
2417
|
+
visibility: hidden;
|
|
2418
|
+
}
|
|
2419
|
+
.composer.wide,
|
|
2420
|
+
.composer.previewing {
|
|
2421
|
+
flex: 1 1 auto;
|
|
2422
|
+
min-height: 0;
|
|
2423
|
+
}
|
|
2424
|
+
.composer.wide .input-row {
|
|
2425
|
+
flex: 1;
|
|
2426
|
+
min-height: 0;
|
|
2427
|
+
align-items: stretch;
|
|
2428
|
+
}
|
|
2429
|
+
.composer.wide .input-row > :not(.textarea-wrap) {
|
|
2430
|
+
align-self: flex-end;
|
|
2431
|
+
}
|
|
2432
|
+
.composer.wide textarea {
|
|
2433
|
+
min-height: 0;
|
|
2434
|
+
}
|
|
2435
|
+
.composer-size {
|
|
2436
|
+
margin-left: auto;
|
|
2437
|
+
}
|
|
2438
|
+
/* Named above the send button, out of the row - it takes no width from the box. */
|
|
2439
|
+
.composer.wide .input-row {
|
|
2440
|
+
position: relative;
|
|
2441
|
+
}
|
|
2442
|
+
.send-shortcut {
|
|
2443
|
+
position: absolute;
|
|
2444
|
+
right: 14px;
|
|
2445
|
+
bottom: 48px;
|
|
2446
|
+
font-size: 10px;
|
|
2447
|
+
color: var(--md-sys-color-outline, #64748b);
|
|
2448
|
+
white-space: nowrap;
|
|
2449
|
+
pointer-events: none;
|
|
2450
|
+
}
|
|
2451
|
+
/* A finger has no Ctrl or ⌘ - the send button is the way. */
|
|
2452
|
+
@media (pointer: coarse) {
|
|
2453
|
+
.send-shortcut {
|
|
2454
|
+
display: none;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
.table-preview {
|
|
2458
|
+
flex: 1;
|
|
2459
|
+
min-height: 0;
|
|
2460
|
+
display: flex;
|
|
2461
|
+
flex-direction: column;
|
|
2462
|
+
margin: 4px 14px;
|
|
2463
|
+
border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
|
|
2464
|
+
border-radius: 8px;
|
|
2465
|
+
background: var(--md-sys-color-surface, #ffffff);
|
|
2466
|
+
overflow: hidden;
|
|
2467
|
+
}
|
|
2468
|
+
.table-preview-head {
|
|
2469
|
+
display: flex;
|
|
2470
|
+
align-items: center;
|
|
2471
|
+
gap: 6px;
|
|
2472
|
+
padding: 6px 8px;
|
|
2473
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
|
|
2474
|
+
flex-wrap: wrap;
|
|
2475
|
+
}
|
|
2476
|
+
.table-preview-head > md-icon {
|
|
2477
|
+
--md-icon-size: 18px;
|
|
2478
|
+
color: var(--md-sys-color-primary, #0284c7);
|
|
2479
|
+
}
|
|
2480
|
+
.table-preview-name {
|
|
2481
|
+
font-weight: 500;
|
|
2482
|
+
color: var(--md-sys-color-on-surface, #0f172a);
|
|
2483
|
+
min-width: 0;
|
|
2484
|
+
overflow: hidden;
|
|
2485
|
+
text-overflow: ellipsis;
|
|
2486
|
+
white-space: nowrap;
|
|
2487
|
+
}
|
|
2488
|
+
.table-preview-shape {
|
|
2489
|
+
color: var(--md-sys-color-on-surface-variant, #475569);
|
|
2490
|
+
margin-right: auto;
|
|
2491
|
+
}
|
|
2492
|
+
.table-preview-body {
|
|
2493
|
+
flex: 1;
|
|
2494
|
+
min-height: 0;
|
|
2495
|
+
overflow: auto;
|
|
2496
|
+
}
|
|
2497
|
+
.table-preview-body table {
|
|
2498
|
+
border-collapse: collapse;
|
|
2499
|
+
font-size: 11px;
|
|
2500
|
+
}
|
|
2501
|
+
.table-preview-body th,
|
|
2502
|
+
.table-preview-body td {
|
|
2503
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
|
|
2504
|
+
padding: 4px 8px;
|
|
2505
|
+
text-align: left;
|
|
2506
|
+
white-space: nowrap;
|
|
2507
|
+
color: var(--md-sys-color-on-surface, #0f172a);
|
|
2508
|
+
}
|
|
2509
|
+
.table-preview-body th {
|
|
2510
|
+
position: sticky;
|
|
2511
|
+
left: 0;
|
|
2512
|
+
font-weight: 400;
|
|
2513
|
+
text-align: right;
|
|
2514
|
+
color: var(--md-sys-color-on-surface-variant, #475569);
|
|
2515
|
+
background: var(--md-sys-color-surface-container, #f1f5f9);
|
|
2516
|
+
}
|
|
2517
|
+
.attachment-open {
|
|
2518
|
+
all: unset;
|
|
2519
|
+
display: flex;
|
|
2520
|
+
align-items: center;
|
|
2521
|
+
gap: 6px;
|
|
2522
|
+
min-width: 0;
|
|
2523
|
+
flex: 1;
|
|
2524
|
+
cursor: pointer;
|
|
2525
|
+
border-radius: 6px;
|
|
2526
|
+
}
|
|
2527
|
+
.attachment-open:focus-visible {
|
|
2528
|
+
outline: 2px solid var(--md-sys-color-primary, #0284c7);
|
|
2529
|
+
outline-offset: 2px;
|
|
2530
|
+
}
|
|
2531
|
+
.attachment-chip.open {
|
|
2532
|
+
border-color: var(--md-sys-color-primary, #0284c7);
|
|
2533
|
+
}
|
|
2366
2534
|
.composer.drag-over {
|
|
2367
2535
|
background: var(--md-sys-color-surface-container, #f1f5f9);
|
|
2368
2536
|
border-top-color: var(--md-sys-color-primary, #0284c7);
|
|
@@ -2629,8 +2797,9 @@ export class OxAssistantChat extends LitElement {
|
|
|
2629
2797
|
font: inherit;
|
|
2630
2798
|
letter-spacing: inherit;
|
|
2631
2799
|
resize: none;
|
|
2632
|
-
|
|
2633
|
-
|
|
2800
|
+
/* The resting height of one line as it stood; the box grows from here by composerHeight (shared-dock.md §10). */
|
|
2801
|
+
box-sizing: border-box;
|
|
2802
|
+
min-height: 54px;
|
|
2634
2803
|
outline: none;
|
|
2635
2804
|
transition:
|
|
2636
2805
|
border-color 0.2s,
|
|
@@ -2733,6 +2902,15 @@ export class OxAssistantChat extends LitElement {
|
|
|
2733
2902
|
/* 모드가 이미 붙어 있으면 무엇을 주는지 먼저 묻는다 — 바뀔 때는 updated 가 다시 묻는다. */
|
|
2734
2903
|
if (this.mode?.kind) this.loadModeView()
|
|
2735
2904
|
if (this.subject) this.onSubjectChanged()
|
|
2905
|
+
/*
|
|
2906
|
+
* Put back after the dock was closed: what is being written stays, the conversation is read again and its
|
|
2907
|
+
* broadcast and presence start again - they were stopped when it was taken out.
|
|
2908
|
+
*/
|
|
2909
|
+
if (this.hasUpdated && this.sessionId) {
|
|
2910
|
+
this.loadHistory()
|
|
2911
|
+
this._startPresence()
|
|
2912
|
+
this._startMessageSubscription()
|
|
2913
|
+
}
|
|
2736
2914
|
}
|
|
2737
2915
|
|
|
2738
2916
|
/**
|
|
@@ -2750,6 +2928,8 @@ export class OxAssistantChat extends LitElement {
|
|
|
2750
2928
|
}
|
|
2751
2929
|
|
|
2752
2930
|
updated(changed: Map<string, any>) {
|
|
2931
|
+
if (changed.has('input') || changed.has('composerWide') || changed.has('previewAttachmentId') || changed.has('attachments')) this.fitComposer()
|
|
2932
|
+
|
|
2753
2933
|
/*
|
|
2754
2934
|
* 밖에서 넣어 준 질문을 자동 전송한다(오류 토스트의 "AI에게 묻기" 등).
|
|
2755
2935
|
*
|
|
@@ -2989,7 +3169,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
2989
3169
|
${this.renderModeSubject()}
|
|
2990
3170
|
${this.renderSessionTabs()}
|
|
2991
3171
|
${this.renderRoster()}
|
|
2992
|
-
<div class="messages" @click=${this.onBubbleClick}>
|
|
3172
|
+
<div class="messages ${this.composerWide || this.previewedTable ? 'covered' : ''}" @click=${this.onBubbleClick}>
|
|
2993
3173
|
${!hasMessages ? this.renderEmpty() : nothing}
|
|
2994
3174
|
${(() => {
|
|
2995
3175
|
const myEmail = this._myEmail
|
|
@@ -3112,7 +3292,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
3112
3292
|
: nothing}
|
|
3113
3293
|
${this.renderFoldNotice()}
|
|
3114
3294
|
<div
|
|
3115
|
-
class="composer ${this.dragOver ? 'drag-over' : ''}"
|
|
3295
|
+
class="composer ${this.dragOver ? 'drag-over' : ''} ${this.composerWide ? 'wide' : ''} ${this.previewedTable ? 'previewing' : ''}"
|
|
3116
3296
|
@dragover=${(e: DragEvent) => this.onComposerDragOver(e)}
|
|
3117
3297
|
@dragleave=${(e: DragEvent) => this.onComposerDragLeave(e)}
|
|
3118
3298
|
@drop=${(e: DragEvent) => this.onComposerDrop(e)}
|
|
@@ -3164,21 +3344,44 @@ export class OxAssistantChat extends LitElement {
|
|
|
3164
3344
|
</button>
|
|
3165
3345
|
`
|
|
3166
3346
|
: nothing}
|
|
3347
|
+
<button
|
|
3348
|
+
type="button"
|
|
3349
|
+
class="action composer-size"
|
|
3350
|
+
aria-pressed=${this.composerWide ? 'true' : 'false'}
|
|
3351
|
+
@click=${() => this.setComposerWide(!this.composerWide)}>
|
|
3352
|
+
<md-icon>${this.composerWide ? 'close_fullscreen' : 'open_in_full'}</md-icon>
|
|
3353
|
+
${this.composerWide
|
|
3354
|
+
? i18next.t('ai-assistant.button.write-narrow', { defaultValue: 'Narrow' })
|
|
3355
|
+
: i18next.t('ai-assistant.button.write-wide', { defaultValue: 'Write wide' })}
|
|
3356
|
+
</button>
|
|
3167
3357
|
</div>
|
|
3358
|
+
${this.renderTablePreview()}
|
|
3168
3359
|
${this.examplesOpen ? this.renderInlineExamples() : nothing}
|
|
3169
3360
|
${this.attachments.length > 0
|
|
3170
3361
|
? html`
|
|
3171
3362
|
<div class="attachment-strip">
|
|
3172
3363
|
${this.attachments.map(
|
|
3173
3364
|
att => html`
|
|
3174
|
-
<div class="attachment-chip" title=${att.name}>
|
|
3175
|
-
${att.
|
|
3176
|
-
? html`<
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3365
|
+
<div class="attachment-chip ${att.id === this.previewAttachmentId ? 'open' : ''}" title=${att.name}>
|
|
3366
|
+
${att.table
|
|
3367
|
+
? html`<button
|
|
3368
|
+
type="button"
|
|
3369
|
+
class="attachment-open"
|
|
3370
|
+
aria-expanded=${att.id === this.previewAttachmentId ? 'true' : 'false'}
|
|
3371
|
+
@click=${() => (this.previewAttachmentId = att.id === this.previewAttachmentId ? null : att.id)}>
|
|
3372
|
+
<md-icon class="attachment-icon">table_chart</md-icon>
|
|
3373
|
+
<span class="attachment-meta">
|
|
3374
|
+
<span class="attachment-name">${att.name}</span>
|
|
3375
|
+
<span class="attachment-size">${this.tableShape(att.table)}</span>
|
|
3376
|
+
</span>
|
|
3377
|
+
</button>`
|
|
3378
|
+
: html`${att.type.startsWith('image/')
|
|
3379
|
+
? html`<img class="attachment-thumb" src=${att.url} alt=${att.name} />`
|
|
3380
|
+
: html`<md-icon class="attachment-icon">description</md-icon>`}
|
|
3381
|
+
<div class="attachment-meta">
|
|
3382
|
+
<span class="attachment-name">${att.name}</span>
|
|
3383
|
+
<span class="attachment-size">${formatFileSize(att.size)}</span>
|
|
3384
|
+
</div>`}
|
|
3182
3385
|
<button
|
|
3183
3386
|
type="button"
|
|
3184
3387
|
class="attachment-remove"
|
|
@@ -3250,10 +3453,16 @@ export class OxAssistantChat extends LitElement {
|
|
|
3250
3453
|
</button>
|
|
3251
3454
|
`
|
|
3252
3455
|
: nothing}
|
|
3456
|
+
${this.composerWide
|
|
3457
|
+
? html`<span class="send-shortcut">${i18next.t('ai-assistant.text.send-shortcut', { key: sendShortcut(navigator.platform), defaultValue: '{key} to send' })}</span>`
|
|
3458
|
+
: nothing}
|
|
3253
3459
|
<button
|
|
3254
3460
|
?disabled=${this.busy || !!this.sendBlocked || (!this.input.trim() && this.attachments.length === 0)}
|
|
3255
3461
|
@click=${this.send}
|
|
3256
|
-
title=${this.sendBlocked ||
|
|
3462
|
+
title=${this.sendBlocked ||
|
|
3463
|
+
(this.composerWide
|
|
3464
|
+
? i18next.t('ai-assistant.text.send-tooltip-wide', { key: sendShortcut(navigator.platform), defaultValue: 'Send ({key})' })
|
|
3465
|
+
: i18next.t('ai-assistant.text.send-tooltip'))}>
|
|
3257
3466
|
${this.busy
|
|
3258
3467
|
? html`<span class="spinner"></span>`
|
|
3259
3468
|
: html`<md-icon>arrow_upward</md-icon>`}
|
|
@@ -3340,6 +3549,60 @@ export class OxAssistantChat extends LitElement {
|
|
|
3340
3549
|
`
|
|
3341
3550
|
}
|
|
3342
3551
|
|
|
3552
|
+
private get previewedTable(): ChatAttachment | undefined {
|
|
3553
|
+
return this.previewAttachmentId ? this.attachments.find(a => a.id === this.previewAttachmentId && a.table) : undefined
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
private tableShape(table: NonNullable<ChatAttachment['table']>) {
|
|
3557
|
+
return i18next.t('ai-assistant.text.table-shape', { rows: table.rows.length, columns: table.columns, defaultValue: '{rows} rows × {columns} columns' })
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
/*
|
|
3561
|
+
The rows of a pasted table, over the conversation - numbered as the file numbers them, which is the number the
|
|
3562
|
+
entry card names. Every row: the paste is held under the mode's limit already.
|
|
3563
|
+
*/
|
|
3564
|
+
private renderTablePreview() {
|
|
3565
|
+
const att = this.previewedTable
|
|
3566
|
+
if (!att?.table) return nothing
|
|
3567
|
+
const { rows, columns } = att.table
|
|
3568
|
+
return html`
|
|
3569
|
+
<div
|
|
3570
|
+
class="table-preview"
|
|
3571
|
+
role="region"
|
|
3572
|
+
aria-label=${att.name}
|
|
3573
|
+
@keydown=${(e: KeyboardEvent) => {
|
|
3574
|
+
if (e.key === 'Escape') {
|
|
3575
|
+
e.stopPropagation()
|
|
3576
|
+
this.previewAttachmentId = null
|
|
3577
|
+
}
|
|
3578
|
+
}}>
|
|
3579
|
+
<div class="table-preview-head">
|
|
3580
|
+
<md-icon>table_chart</md-icon>
|
|
3581
|
+
<span class="table-preview-name">${att.name}</span>
|
|
3582
|
+
<span class="table-preview-shape">${this.tableShape(att.table)}</span>
|
|
3583
|
+
<button type="button" class="action" @click=${() => this.tableToWords(att.id)}>
|
|
3584
|
+
<md-icon>notes</md-icon>
|
|
3585
|
+
${i18next.t('ai-assistant.button.table-to-words', { defaultValue: 'Put in as words' })}
|
|
3586
|
+
</button>
|
|
3587
|
+
<button type="button" class="action" @click=${() => (this.previewAttachmentId = null)}>
|
|
3588
|
+
<md-icon>close</md-icon>
|
|
3589
|
+
${i18next.t('ai-assistant.button.close')}
|
|
3590
|
+
</button>
|
|
3591
|
+
</div>
|
|
3592
|
+
<div class="table-preview-body">
|
|
3593
|
+
<table>
|
|
3594
|
+
${rows.map(
|
|
3595
|
+
(row, i) => html`<tr>
|
|
3596
|
+
<th scope="row">${i + 1}</th>
|
|
3597
|
+
${Array.from({ length: columns }, (_, c) => html`<td>${row[c] ?? ''}</td>`)}
|
|
3598
|
+
</tr>`
|
|
3599
|
+
)}
|
|
3600
|
+
</table>
|
|
3601
|
+
</div>
|
|
3602
|
+
</div>
|
|
3603
|
+
`
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3343
3606
|
private onKeyDown(e: KeyboardEvent) {
|
|
3344
3607
|
/*
|
|
3345
3608
|
The Enter that ends a Korean (or Japanese, Chinese) syllable belongs to the input method - sending on it
|
|
@@ -3382,9 +3645,15 @@ export class OxAssistantChat extends LitElement {
|
|
|
3382
3645
|
return
|
|
3383
3646
|
}
|
|
3384
3647
|
}
|
|
3385
|
-
|
|
3648
|
+
const action = composerKeyAction(e, this.composerWide)
|
|
3649
|
+
if (action === 'narrow') {
|
|
3650
|
+
/* The Escape that narrows the box is spent here - the floating dock closes on the next one. */
|
|
3386
3651
|
e.preventDefault()
|
|
3387
|
-
|
|
3652
|
+
e.stopPropagation()
|
|
3653
|
+
this.setComposerWide(false)
|
|
3654
|
+
} else if (action === 'send') {
|
|
3655
|
+
e.preventDefault()
|
|
3656
|
+
if (!this.busy) this.send()
|
|
3388
3657
|
}
|
|
3389
3658
|
}
|
|
3390
3659
|
|
|
@@ -4687,6 +4956,9 @@ export class OxAssistantChat extends LitElement {
|
|
|
4687
4956
|
const attachmentsToSend = [...this.attachments]
|
|
4688
4957
|
if ((!text && attachmentsToSend.length === 0) || this.busy) return
|
|
4689
4958
|
this.attachments = []
|
|
4959
|
+
/* What was written wide is sent to be answered - the conversation comes back to show it. */
|
|
4960
|
+
this.composerWide = false
|
|
4961
|
+
this.previewAttachmentId = null
|
|
4690
4962
|
|
|
4691
4963
|
const promptText = text || (attachmentsToSend.length > 0
|
|
4692
4964
|
? (attachmentsToSend[0].type.startsWith('image/')
|
|
@@ -360,16 +360,21 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
360
360
|
if (activity.kind === 'meta' && isAnchor(this.anchor) && activity.anchorId === this.anchor.anchorId) void this.reloadSessions()
|
|
361
361
|
})
|
|
362
362
|
)
|
|
363
|
-
this.follow(true)
|
|
363
|
+
if (!this.openedIdentity) return this.follow(true)
|
|
364
|
+
/*
|
|
365
|
+
* The same dock opened again - the aside bar takes it out while it is closed. The conversation and what was
|
|
366
|
+
* being written in it are still here; only what moved while it was closed (the screen, the person) is followed.
|
|
367
|
+
*/
|
|
368
|
+
setOpenSession(this.sessionId)
|
|
369
|
+
this.follow()
|
|
364
370
|
}
|
|
365
371
|
|
|
372
|
+
/* Closing keeps the conversation: the dock comes back as it was left (design/ai/shared-dock.md §2-1, §2-7). */
|
|
366
373
|
disconnectedCallback() {
|
|
367
|
-
this.revision++
|
|
368
374
|
this.removeEventListener('assistant-start-new-session', this.onStartNewSession)
|
|
369
375
|
this.removeEventListener('assistant-open-page', this.onOpenPage as EventListener)
|
|
370
376
|
this.unsubscribers.splice(0).forEach(unsubscribe => unsubscribe())
|
|
371
377
|
setOpenSession('')
|
|
372
|
-
this.controller.clear()
|
|
373
378
|
super.disconnectedCallback()
|
|
374
379
|
}
|
|
375
380
|
|
|
@@ -124,6 +124,14 @@ export function setupAssistantDock(options: { viewpart?: string } = {}): void {
|
|
|
124
124
|
|
|
125
125
|
startSessionActivityFeed()
|
|
126
126
|
|
|
127
|
+
/*
|
|
128
|
+
* One dock for the application's life. The aside bar drops a closed viewpart's template, so a template that
|
|
129
|
+
* makes the element would make a new dock on every opening - and a new composer, the text being written gone
|
|
130
|
+
* (design/ai/shared-dock.md §2-1, §2-7). The same element is put back instead, as the entity360 dock is.
|
|
131
|
+
*/
|
|
132
|
+
const dock = document.createElement('ox-assistant-dock') as HTMLElement & { viewpart: string }
|
|
133
|
+
dock.viewpart = viewpart
|
|
134
|
+
|
|
127
135
|
appendViewpart({
|
|
128
136
|
name: viewpart,
|
|
129
137
|
/*
|
|
@@ -134,7 +142,7 @@ export function setupAssistantDock(options: { viewpart?: string } = {}): void {
|
|
|
134
142
|
show: false,
|
|
135
143
|
persistent: true,
|
|
136
144
|
level: VIEWPART_LEVEL.TOPMOST,
|
|
137
|
-
template: html
|
|
145
|
+
template: html`${dock}`
|
|
138
146
|
},
|
|
139
147
|
position: VIEWPART_POSITION.ASIDEBAR
|
|
140
148
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The composer's frame (operato-application/design/ai/shared-dock.md §10) - how tall it stands, what Enter does,
|
|
3
|
+
* and what a pasted table gives back as words. Pure: the chat hands in its measures and the key.
|
|
4
|
+
*
|
|
5
|
+
* - The box grows with its text up to 40% of the chat's height, then scrolls. It never stands under its resting
|
|
6
|
+
* height, and on a chat too short for 40% to hold that height the resting height wins.
|
|
7
|
+
* - The wide box (넓게 쓰기) covers the conversation inside the dock; the page beside it stays where it was.
|
|
8
|
+
* - In the small box Enter sends and Shift+Enter breaks the line, as in every chat. In the wide box Enter breaks
|
|
9
|
+
* the line - long writing is made of lines - and Ctrl/Cmd+Enter sends. Ctrl/Cmd+Enter sends in both.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The part of the chat's height the small box may take before it scrolls. */
|
|
13
|
+
export const COMPOSER_GROWTH = 0.4
|
|
14
|
+
|
|
15
|
+
/** The box's height for its content, both border-box pixels. */
|
|
16
|
+
export function composerHeight(content: number, chat: number, resting: number): { height: number; scrolls: boolean } {
|
|
17
|
+
const cap = Math.max(resting, Math.floor(chat * COMPOSER_GROWTH))
|
|
18
|
+
return { height: Math.min(Math.max(content, resting), cap), scrolls: content > cap }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ComposerKey = { key: string; shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean; altKey?: boolean }
|
|
22
|
+
|
|
23
|
+
/** What a key does in the box: send, close the wide box, or nothing of ours (the textarea keeps it). */
|
|
24
|
+
export function composerKeyAction(e: ComposerKey, wide: boolean): 'send' | 'narrow' | null {
|
|
25
|
+
if (e.key === 'Escape') return wide ? 'narrow' : null
|
|
26
|
+
if (e.key !== 'Enter' || e.altKey) return null
|
|
27
|
+
if (e.ctrlKey || e.metaKey) return 'send'
|
|
28
|
+
if (wide || e.shiftKey) return null
|
|
29
|
+
return 'send'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The shortcut the wide box's send button names, in the keys of this machine. */
|
|
33
|
+
export function sendShortcut(platform: string | undefined): string {
|
|
34
|
+
return /mac|iphone|ipad/i.test(platform ?? '') ? '⌘ Enter' : 'Ctrl+Enter'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A pasted table given back as words: what the clipboard held as text, or - when it held only HTML - the rows with
|
|
39
|
+
* a tab between cells, as a spreadsheet copies them.
|
|
40
|
+
*/
|
|
41
|
+
export function tableAsWords(table: { rows: string[][]; text?: string | null }): string {
|
|
42
|
+
const text = (table.text ?? '').replace(/\r\n?/g, '\n').replace(/\n+$/, '')
|
|
43
|
+
return text.trim() ? text : table.rows.map(row => row.join('\t')).join('\n')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The words appended to what is already written - on a line of their own. */
|
|
47
|
+
export function appendWords(written: string, words: string): string {
|
|
48
|
+
return written.trim() ? `${written.replace(/\s+$/, '')}\n${words}` : words
|
|
49
|
+
}
|
|
@@ -133,7 +133,7 @@ export function pastedTableFile(
|
|
|
133
133
|
clipboard: { html?: string | null; text?: string | null },
|
|
134
134
|
policy: AttachmentPolicy | null | undefined,
|
|
135
135
|
nameOf: (rows: number) => string
|
|
136
|
-
): { name: string; text: string } | { refusal: PasteRefusal } | null {
|
|
136
|
+
): { name: string; text: string; table: PastedTable } | { refusal: PasteRefusal } | null {
|
|
137
137
|
if (!policy?.kinds?.some(kind => kind === 'text' || kind === 'spreadsheet')) return null
|
|
138
138
|
const table = pastedTable(clipboard)
|
|
139
139
|
if (!table) return null
|
|
@@ -141,5 +141,5 @@ export function pastedTableFile(
|
|
|
141
141
|
const text = pastedTableText(table, name)
|
|
142
142
|
const maxChars = textCharsOf(policy)
|
|
143
143
|
if (text.length > maxChars) return { refusal: { code: 'attachment-paste-too-long', rows: table.rows.length, maxChars } }
|
|
144
|
-
return { name: `${name.replace(/[\\/:*?"<>|]/g, '_')}.csv`, text }
|
|
144
|
+
return { name: `${name.replace(/[\\/:*?"<>|]/g, '_')}.csv`, text, table }
|
|
145
145
|
}
|
|
@@ -29,6 +29,12 @@ export interface ChatAttachment {
|
|
|
29
29
|
type: string;
|
|
30
30
|
url: string;
|
|
31
31
|
base64?: string;
|
|
32
|
+
/** A table pasted into the composer - its shape for the chip, its rows for the preview, its words to give back. */
|
|
33
|
+
table?: {
|
|
34
|
+
rows: string[][];
|
|
35
|
+
columns: number;
|
|
36
|
+
text?: string;
|
|
37
|
+
};
|
|
32
38
|
}
|
|
33
39
|
export declare class OxAssistantChat extends LitElement {
|
|
34
40
|
/** 영속 세션 id. 없으면 ad-hoc (메시지 영속 안 됨). */
|
|
@@ -227,6 +233,10 @@ export declare class OxAssistantChat extends LitElement {
|
|
|
227
233
|
private attachments;
|
|
228
234
|
/** 드래그 오버 상태 (시각 피드백용) */
|
|
229
235
|
private dragOver;
|
|
236
|
+
/** The wide box (넓게 쓰기) covers the conversation inside the dock - shared-dock.md §10. */
|
|
237
|
+
private composerWide;
|
|
238
|
+
/** The pasted table whose rows cover the conversation, or null. */
|
|
239
|
+
private previewAttachmentId;
|
|
230
240
|
/** This conversation talks to the neutral door - its attachments follow the mode's policy (§9). */
|
|
231
241
|
private get usesAttachmentPolicy();
|
|
232
242
|
/** Whether the attach button stands: a mode that declares nothing takes nothing (the board door keeps its images). */
|
|
@@ -238,6 +248,11 @@ export declare class OxAssistantChat extends LitElement {
|
|
|
238
248
|
private readSpreadsheets;
|
|
239
249
|
private readFileAsBase64;
|
|
240
250
|
private removeAttachment;
|
|
251
|
+
/** A pasted table goes back into the box as the words it was copied as - the conversion was not the person's choice. */
|
|
252
|
+
private tableToWords;
|
|
253
|
+
/** The small box stands as tall as its text, up to 40% of the chat; the wide box fills what it covers. */
|
|
254
|
+
private fitComposer;
|
|
255
|
+
private setComposerWide;
|
|
241
256
|
private onComposerPaste;
|
|
242
257
|
private onComposerDragOver;
|
|
243
258
|
private onComposerDragLeave;
|
|
@@ -427,6 +442,9 @@ export declare class OxAssistantChat extends LitElement {
|
|
|
427
442
|
* 일어나지 않는 것이 가장 나쁘다.
|
|
428
443
|
*/
|
|
429
444
|
private renderInviteNotice;
|
|
445
|
+
private get previewedTable();
|
|
446
|
+
private tableShape;
|
|
447
|
+
private renderTablePreview;
|
|
430
448
|
private onKeyDown;
|
|
431
449
|
/** input 이벤트 — value 즉시 반영 + popup 갱신은 150ms 디바운스 (F-Finder 와 동일).
|
|
432
450
|
* big board 에서 매 keystroke filter 비용 절감 + 한국어 IME 조합 중 깜빡임 완화. */
|