@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.
@@ -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
- this.onAttachFiles([new File([pasted.text], pasted.name, { type: 'text/csv' })])
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
- min-height: 36px;
2633
- max-height: 140px;
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.type.startsWith('image/')
3176
- ? html`<img class="attachment-thumb" src=${att.url} alt=${att.name} />`
3177
- : html`<md-icon class="attachment-icon">description</md-icon>`}
3178
- <div class="attachment-meta">
3179
- <span class="attachment-name">${att.name}</span>
3180
- <span class="attachment-size">${formatFileSize(att.size)}</span>
3181
- </div>
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 || i18next.t('ai-assistant.text.send-tooltip')}>
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
- if (e.key === 'Enter' && !e.shiftKey && !this.busy) {
3617
+ const action = composerKeyAction(e, this.composerWide)
3618
+ if (action === 'narrow') {
3386
3619
  e.preventDefault()
3387
- this.send()
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 조합 중 깜빡임 완화. */