@things-factory/ai-assistant 10.1.109 → 10.1.110
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 +257 -18
- 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 +258 -18
- package/dist-client/components/assistant-chat.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/package.json +2 -2
- package/test/composer-frame.test.ts +109 -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,115 @@ 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
|
+
.send-shortcut {
|
|
2439
|
+
font-size: 10px;
|
|
2440
|
+
color: var(--md-sys-color-outline, #64748b);
|
|
2441
|
+
white-space: nowrap;
|
|
2442
|
+
padding-bottom: 10px;
|
|
2443
|
+
}
|
|
2444
|
+
.table-preview {
|
|
2445
|
+
flex: 1;
|
|
2446
|
+
min-height: 0;
|
|
2447
|
+
display: flex;
|
|
2448
|
+
flex-direction: column;
|
|
2449
|
+
margin: 4px 14px;
|
|
2450
|
+
border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
|
|
2451
|
+
border-radius: 8px;
|
|
2452
|
+
background: var(--md-sys-color-surface, #ffffff);
|
|
2453
|
+
overflow: hidden;
|
|
2454
|
+
}
|
|
2455
|
+
.table-preview-head {
|
|
2456
|
+
display: flex;
|
|
2457
|
+
align-items: center;
|
|
2458
|
+
gap: 6px;
|
|
2459
|
+
padding: 6px 8px;
|
|
2460
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
|
|
2461
|
+
flex-wrap: wrap;
|
|
2462
|
+
}
|
|
2463
|
+
.table-preview-head > md-icon {
|
|
2464
|
+
--md-icon-size: 18px;
|
|
2465
|
+
color: var(--md-sys-color-primary, #0284c7);
|
|
2466
|
+
}
|
|
2467
|
+
.table-preview-name {
|
|
2468
|
+
font-weight: 500;
|
|
2469
|
+
color: var(--md-sys-color-on-surface, #0f172a);
|
|
2470
|
+
min-width: 0;
|
|
2471
|
+
overflow: hidden;
|
|
2472
|
+
text-overflow: ellipsis;
|
|
2473
|
+
white-space: nowrap;
|
|
2474
|
+
}
|
|
2475
|
+
.table-preview-shape {
|
|
2476
|
+
color: var(--md-sys-color-on-surface-variant, #475569);
|
|
2477
|
+
margin-right: auto;
|
|
2478
|
+
}
|
|
2479
|
+
.table-preview-body {
|
|
2480
|
+
flex: 1;
|
|
2481
|
+
min-height: 0;
|
|
2482
|
+
overflow: auto;
|
|
2483
|
+
}
|
|
2484
|
+
.table-preview-body table {
|
|
2485
|
+
border-collapse: collapse;
|
|
2486
|
+
font-size: 11px;
|
|
2487
|
+
}
|
|
2488
|
+
.table-preview-body th,
|
|
2489
|
+
.table-preview-body td {
|
|
2490
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
|
|
2491
|
+
padding: 4px 8px;
|
|
2492
|
+
text-align: left;
|
|
2493
|
+
white-space: nowrap;
|
|
2494
|
+
color: var(--md-sys-color-on-surface, #0f172a);
|
|
2495
|
+
}
|
|
2496
|
+
.table-preview-body th {
|
|
2497
|
+
position: sticky;
|
|
2498
|
+
left: 0;
|
|
2499
|
+
font-weight: 400;
|
|
2500
|
+
text-align: right;
|
|
2501
|
+
color: var(--md-sys-color-on-surface-variant, #475569);
|
|
2502
|
+
background: var(--md-sys-color-surface-container, #f1f5f9);
|
|
2503
|
+
}
|
|
2504
|
+
.attachment-open {
|
|
2505
|
+
all: unset;
|
|
2506
|
+
display: flex;
|
|
2507
|
+
align-items: center;
|
|
2508
|
+
gap: 6px;
|
|
2509
|
+
min-width: 0;
|
|
2510
|
+
flex: 1;
|
|
2511
|
+
cursor: pointer;
|
|
2512
|
+
border-radius: 6px;
|
|
2513
|
+
}
|
|
2514
|
+
.attachment-open:focus-visible {
|
|
2515
|
+
outline: 2px solid var(--md-sys-color-primary, #0284c7);
|
|
2516
|
+
outline-offset: 2px;
|
|
2517
|
+
}
|
|
2518
|
+
.attachment-chip.open {
|
|
2519
|
+
border-color: var(--md-sys-color-primary, #0284c7);
|
|
2520
|
+
}
|
|
2366
2521
|
.composer.drag-over {
|
|
2367
2522
|
background: var(--md-sys-color-surface-container, #f1f5f9);
|
|
2368
2523
|
border-top-color: var(--md-sys-color-primary, #0284c7);
|
|
@@ -2629,8 +2784,9 @@ export class OxAssistantChat extends LitElement {
|
|
|
2629
2784
|
font: inherit;
|
|
2630
2785
|
letter-spacing: inherit;
|
|
2631
2786
|
resize: none;
|
|
2632
|
-
|
|
2633
|
-
|
|
2787
|
+
/* The resting height of one line as it stood; the box grows from here by composerHeight (shared-dock.md §10). */
|
|
2788
|
+
box-sizing: border-box;
|
|
2789
|
+
min-height: 54px;
|
|
2634
2790
|
outline: none;
|
|
2635
2791
|
transition:
|
|
2636
2792
|
border-color 0.2s,
|
|
@@ -2750,6 +2906,8 @@ export class OxAssistantChat extends LitElement {
|
|
|
2750
2906
|
}
|
|
2751
2907
|
|
|
2752
2908
|
updated(changed: Map<string, any>) {
|
|
2909
|
+
if (changed.has('input') || changed.has('composerWide') || changed.has('previewAttachmentId') || changed.has('attachments')) this.fitComposer()
|
|
2910
|
+
|
|
2753
2911
|
/*
|
|
2754
2912
|
* 밖에서 넣어 준 질문을 자동 전송한다(오류 토스트의 "AI에게 묻기" 등).
|
|
2755
2913
|
*
|
|
@@ -2989,7 +3147,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
2989
3147
|
${this.renderModeSubject()}
|
|
2990
3148
|
${this.renderSessionTabs()}
|
|
2991
3149
|
${this.renderRoster()}
|
|
2992
|
-
<div class="messages" @click=${this.onBubbleClick}>
|
|
3150
|
+
<div class="messages ${this.composerWide || this.previewedTable ? 'covered' : ''}" @click=${this.onBubbleClick}>
|
|
2993
3151
|
${!hasMessages ? this.renderEmpty() : nothing}
|
|
2994
3152
|
${(() => {
|
|
2995
3153
|
const myEmail = this._myEmail
|
|
@@ -3112,7 +3270,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
3112
3270
|
: nothing}
|
|
3113
3271
|
${this.renderFoldNotice()}
|
|
3114
3272
|
<div
|
|
3115
|
-
class="composer ${this.dragOver ? 'drag-over' : ''}"
|
|
3273
|
+
class="composer ${this.dragOver ? 'drag-over' : ''} ${this.composerWide ? 'wide' : ''} ${this.previewedTable ? 'previewing' : ''}"
|
|
3116
3274
|
@dragover=${(e: DragEvent) => this.onComposerDragOver(e)}
|
|
3117
3275
|
@dragleave=${(e: DragEvent) => this.onComposerDragLeave(e)}
|
|
3118
3276
|
@drop=${(e: DragEvent) => this.onComposerDrop(e)}
|
|
@@ -3164,21 +3322,44 @@ export class OxAssistantChat extends LitElement {
|
|
|
3164
3322
|
</button>
|
|
3165
3323
|
`
|
|
3166
3324
|
: nothing}
|
|
3325
|
+
<button
|
|
3326
|
+
type="button"
|
|
3327
|
+
class="action composer-size"
|
|
3328
|
+
aria-pressed=${this.composerWide ? 'true' : 'false'}
|
|
3329
|
+
@click=${() => this.setComposerWide(!this.composerWide)}>
|
|
3330
|
+
<md-icon>${this.composerWide ? 'close_fullscreen' : 'open_in_full'}</md-icon>
|
|
3331
|
+
${this.composerWide
|
|
3332
|
+
? i18next.t('ai-assistant.button.write-narrow', { defaultValue: 'Narrow' })
|
|
3333
|
+
: i18next.t('ai-assistant.button.write-wide', { defaultValue: 'Write wide' })}
|
|
3334
|
+
</button>
|
|
3167
3335
|
</div>
|
|
3336
|
+
${this.renderTablePreview()}
|
|
3168
3337
|
${this.examplesOpen ? this.renderInlineExamples() : nothing}
|
|
3169
3338
|
${this.attachments.length > 0
|
|
3170
3339
|
? html`
|
|
3171
3340
|
<div class="attachment-strip">
|
|
3172
3341
|
${this.attachments.map(
|
|
3173
3342
|
att => html`
|
|
3174
|
-
<div class="attachment-chip" title=${att.name}>
|
|
3175
|
-
${att.
|
|
3176
|
-
? html`<
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3343
|
+
<div class="attachment-chip ${att.id === this.previewAttachmentId ? 'open' : ''}" title=${att.name}>
|
|
3344
|
+
${att.table
|
|
3345
|
+
? html`<button
|
|
3346
|
+
type="button"
|
|
3347
|
+
class="attachment-open"
|
|
3348
|
+
aria-expanded=${att.id === this.previewAttachmentId ? 'true' : 'false'}
|
|
3349
|
+
@click=${() => (this.previewAttachmentId = att.id === this.previewAttachmentId ? null : att.id)}>
|
|
3350
|
+
<md-icon class="attachment-icon">table_chart</md-icon>
|
|
3351
|
+
<span class="attachment-meta">
|
|
3352
|
+
<span class="attachment-name">${att.name}</span>
|
|
3353
|
+
<span class="attachment-size">${this.tableShape(att.table)}</span>
|
|
3354
|
+
</span>
|
|
3355
|
+
</button>`
|
|
3356
|
+
: html`${att.type.startsWith('image/')
|
|
3357
|
+
? html`<img class="attachment-thumb" src=${att.url} alt=${att.name} />`
|
|
3358
|
+
: html`<md-icon class="attachment-icon">description</md-icon>`}
|
|
3359
|
+
<div class="attachment-meta">
|
|
3360
|
+
<span class="attachment-name">${att.name}</span>
|
|
3361
|
+
<span class="attachment-size">${formatFileSize(att.size)}</span>
|
|
3362
|
+
</div>`}
|
|
3182
3363
|
<button
|
|
3183
3364
|
type="button"
|
|
3184
3365
|
class="attachment-remove"
|
|
@@ -3250,10 +3431,16 @@ export class OxAssistantChat extends LitElement {
|
|
|
3250
3431
|
</button>
|
|
3251
3432
|
`
|
|
3252
3433
|
: nothing}
|
|
3434
|
+
${this.composerWide
|
|
3435
|
+
? html`<span class="send-shortcut">${i18next.t('ai-assistant.text.send-shortcut', { key: sendShortcut(navigator.platform), defaultValue: '{key} to send' })}</span>`
|
|
3436
|
+
: nothing}
|
|
3253
3437
|
<button
|
|
3254
3438
|
?disabled=${this.busy || !!this.sendBlocked || (!this.input.trim() && this.attachments.length === 0)}
|
|
3255
3439
|
@click=${this.send}
|
|
3256
|
-
title=${this.sendBlocked ||
|
|
3440
|
+
title=${this.sendBlocked ||
|
|
3441
|
+
(this.composerWide
|
|
3442
|
+
? i18next.t('ai-assistant.text.send-tooltip-wide', { key: sendShortcut(navigator.platform), defaultValue: 'Send ({key})' })
|
|
3443
|
+
: i18next.t('ai-assistant.text.send-tooltip'))}>
|
|
3257
3444
|
${this.busy
|
|
3258
3445
|
? html`<span class="spinner"></span>`
|
|
3259
3446
|
: html`<md-icon>arrow_upward</md-icon>`}
|
|
@@ -3340,6 +3527,51 @@ export class OxAssistantChat extends LitElement {
|
|
|
3340
3527
|
`
|
|
3341
3528
|
}
|
|
3342
3529
|
|
|
3530
|
+
private get previewedTable(): ChatAttachment | undefined {
|
|
3531
|
+
return this.previewAttachmentId ? this.attachments.find(a => a.id === this.previewAttachmentId && a.table) : undefined
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
private tableShape(table: NonNullable<ChatAttachment['table']>) {
|
|
3535
|
+
return i18next.t('ai-assistant.text.table-shape', { rows: table.rows.length, columns: table.columns, defaultValue: '{rows} rows × {columns} columns' })
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
/*
|
|
3539
|
+
The rows of a pasted table, over the conversation - numbered as the file numbers them, which is the number the
|
|
3540
|
+
entry card names. Every row: the paste is held under the mode's limit already.
|
|
3541
|
+
*/
|
|
3542
|
+
private renderTablePreview() {
|
|
3543
|
+
const att = this.previewedTable
|
|
3544
|
+
if (!att?.table) return nothing
|
|
3545
|
+
const { rows, columns } = att.table
|
|
3546
|
+
return html`
|
|
3547
|
+
<div class="table-preview" role="region" aria-label=${att.name}>
|
|
3548
|
+
<div class="table-preview-head">
|
|
3549
|
+
<md-icon>table_chart</md-icon>
|
|
3550
|
+
<span class="table-preview-name">${att.name}</span>
|
|
3551
|
+
<span class="table-preview-shape">${this.tableShape(att.table)}</span>
|
|
3552
|
+
<button type="button" class="action" @click=${() => this.tableToWords(att.id)}>
|
|
3553
|
+
<md-icon>notes</md-icon>
|
|
3554
|
+
${i18next.t('ai-assistant.button.table-to-words', { defaultValue: 'Put in as words' })}
|
|
3555
|
+
</button>
|
|
3556
|
+
<button type="button" class="action" @click=${() => (this.previewAttachmentId = null)}>
|
|
3557
|
+
<md-icon>close</md-icon>
|
|
3558
|
+
${i18next.t('ai-assistant.button.close')}
|
|
3559
|
+
</button>
|
|
3560
|
+
</div>
|
|
3561
|
+
<div class="table-preview-body">
|
|
3562
|
+
<table>
|
|
3563
|
+
${rows.map(
|
|
3564
|
+
(row, i) => html`<tr>
|
|
3565
|
+
<th scope="row">${i + 1}</th>
|
|
3566
|
+
${Array.from({ length: columns }, (_, c) => html`<td>${row[c] ?? ''}</td>`)}
|
|
3567
|
+
</tr>`
|
|
3568
|
+
)}
|
|
3569
|
+
</table>
|
|
3570
|
+
</div>
|
|
3571
|
+
</div>
|
|
3572
|
+
`
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3343
3575
|
private onKeyDown(e: KeyboardEvent) {
|
|
3344
3576
|
/*
|
|
3345
3577
|
The Enter that ends a Korean (or Japanese, Chinese) syllable belongs to the input method - sending on it
|
|
@@ -3382,9 +3614,13 @@ export class OxAssistantChat extends LitElement {
|
|
|
3382
3614
|
return
|
|
3383
3615
|
}
|
|
3384
3616
|
}
|
|
3385
|
-
|
|
3617
|
+
const action = composerKeyAction(e, this.composerWide)
|
|
3618
|
+
if (action === 'narrow') {
|
|
3386
3619
|
e.preventDefault()
|
|
3387
|
-
this.
|
|
3620
|
+
this.setComposerWide(false)
|
|
3621
|
+
} else if (action === 'send') {
|
|
3622
|
+
e.preventDefault()
|
|
3623
|
+
if (!this.busy) this.send()
|
|
3388
3624
|
}
|
|
3389
3625
|
}
|
|
3390
3626
|
|
|
@@ -4687,6 +4923,9 @@ export class OxAssistantChat extends LitElement {
|
|
|
4687
4923
|
const attachmentsToSend = [...this.attachments]
|
|
4688
4924
|
if ((!text && attachmentsToSend.length === 0) || this.busy) return
|
|
4689
4925
|
this.attachments = []
|
|
4926
|
+
/* What was written wide is sent to be answered - the conversation comes back to show it. */
|
|
4927
|
+
this.composerWide = false
|
|
4928
|
+
this.previewAttachmentId = null
|
|
4690
4929
|
|
|
4691
4930
|
const promptText = text || (attachmentsToSend.length > 0
|
|
4692
4931
|
? (attachmentsToSend[0].type.startsWith('image/')
|
|
@@ -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 조합 중 깜빡임 완화. */
|