@things-factory/ai-assistant 10.1.108 → 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.
@@ -29,6 +29,7 @@ import gql from 'graphql-tag';
29
29
  import { renderMarkdown, injectMentionRefids, injectMentionUserIds, stripMentionRefids } from './markdown.js';
30
30
  import { acceptOf, chatAttachmentUrl, refuseAttachments, textCharsOf } from '../utils/attachment-policy.js';
31
31
  import { pastedTableFile } from '../utils/pasted-table.js';
32
+ import { appendWords, composerHeight, composerKeyAction, sendShortcut, tableAsWords } from '../utils/composer-frame.js';
32
33
  import { readSpreadsheet, spreadsheetKindOf } from '../utils/spreadsheet-text.js';
33
34
  import { entryReviewAddress, offersWorkspaceReview } from '../utils/entry-workspace.js';
34
35
  import { buildAssistantChatInput, buildChatMutationInput } from './chat-input-builder.js';
@@ -295,6 +296,10 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
295
296
  this.attachments = [];
296
297
  /** 드래그 오버 상태 (시각 피드백용) */
297
298
  this.dragOver = false;
299
+ /** The wide box (넓게 쓰기) covers the conversation inside the dock - shared-dock.md §10. */
300
+ this.composerWide = false;
301
+ /** The pasted table whose rows cover the conversation, or null. */
302
+ this.previewAttachmentId = null;
298
303
  /** mini action — 인라인 예시 토글 */
299
304
  this.examplesOpen = false;
300
305
  /** 원본 JSON 을 펼친 단계(라인:단계) — 목록은 한 줄 요약으로 훑고 필요한 것만 펼친다. */
@@ -449,7 +454,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
449
454
  params.kinds = refusal.kinds.join(', ');
450
455
  return String(i18next.t(`ai-assistant.text.${refusal.code}`, { ...params, defaultValue: refusal.code }));
451
456
  }
452
- async onAttachFiles(files) {
457
+ async onAttachFiles(files, table) {
453
458
  if (!files || files.length === 0)
454
459
  return;
455
460
  let list = Array.from(files);
@@ -488,7 +493,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
488
493
  size: file.size,
489
494
  type: file.type || 'application/octet-stream',
490
495
  url,
491
- base64
496
+ base64,
497
+ table
492
498
  });
493
499
  }
494
500
  this.attachments = [...this.attachments, ...newAttachments];
@@ -531,6 +537,40 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
531
537
  URL.revokeObjectURL(target.url);
532
538
  }
533
539
  this.attachments = this.attachments.filter(a => a.id !== id);
540
+ if (this.previewAttachmentId === id)
541
+ this.previewAttachmentId = null;
542
+ }
543
+ /** A pasted table goes back into the box as the words it was copied as - the conversion was not the person's choice. */
544
+ tableToWords(id) {
545
+ const target = this.attachments.find(a => a.id === id);
546
+ if (!target?.table)
547
+ return;
548
+ this.input = appendWords(this.input, tableAsWords(target.table));
549
+ this.removeAttachment(id);
550
+ this.updateComplete.then(() => this.renderRoot.querySelector('textarea')?.focus());
551
+ }
552
+ /** The small box stands as tall as its text, up to 40% of the chat; the wide box fills what it covers. */
553
+ fitComposer() {
554
+ const ta = this.renderRoot.querySelector('.input-row textarea');
555
+ if (!ta)
556
+ return;
557
+ if (this.composerWide) {
558
+ ta.style.height = '';
559
+ ta.style.overflowY = '';
560
+ return;
561
+ }
562
+ ta.style.height = '0px';
563
+ const border = ta.offsetHeight - ta.clientHeight;
564
+ const resting = parseFloat(getComputedStyle(ta).minHeight) || 0;
565
+ const { height, scrolls } = composerHeight(ta.scrollHeight + border, this.clientHeight, resting);
566
+ ta.style.height = `${height}px`;
567
+ ta.style.overflowY = scrolls ? 'auto' : 'hidden';
568
+ }
569
+ setComposerWide(wide) {
570
+ this.composerWide = wide;
571
+ if (wide)
572
+ this.previewAttachmentId = null;
573
+ this.updateComplete.then(() => this.renderRoot.querySelector('.input-row textarea')?.focus());
534
574
  }
535
575
  onComposerPaste(e) {
536
576
  const items = e.clipboardData?.items;
@@ -548,7 +588,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
548
588
  this.errorMessage = String(i18next.t(`ai-assistant.text.${pasted.refusal.code}`, { ...pasted.refusal, defaultValue: pasted.refusal.code }));
549
589
  return;
550
590
  }
551
- this.onAttachFiles([new File([pasted.text], pasted.name, { type: 'text/csv' })]);
591
+ const words = e.clipboardData?.getData('text/plain');
592
+ this.onAttachFiles([new File([pasted.text], pasted.name, { type: 'text/csv' })], { ...pasted.table, text: words });
552
593
  return;
553
594
  }
554
595
  }
@@ -1671,6 +1712,42 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
1671
1712
  color: inherit;
1672
1713
  }
1673
1714
 
1715
+ /* The entry card's way to the large review - a quiet action under its table, never a second run button. */
1716
+ .workspace-review {
1717
+ display: flex;
1718
+ align-items: center;
1719
+ gap: 5px;
1720
+ margin: 6px 0 0 auto;
1721
+ padding: 5px 11px 5px 9px;
1722
+ border: 1px solid transparent;
1723
+ border-radius: 999px;
1724
+ background: transparent;
1725
+ color: var(--md-sys-color-primary, #0f766e);
1726
+ font-family: inherit;
1727
+ font-size: 12px;
1728
+ font-weight: 600;
1729
+ letter-spacing: 0.01em;
1730
+ cursor: pointer;
1731
+ transition:
1732
+ background-color 0.14s ease-in-out,
1733
+ border-color 0.14s ease-in-out;
1734
+ }
1735
+ .workspace-review md-icon {
1736
+ --md-icon-size: 15px;
1737
+ color: inherit;
1738
+ }
1739
+ .workspace-review:hover {
1740
+ background: color-mix(in srgb, var(--md-sys-color-primary, #0f766e) 8%, transparent);
1741
+ border-color: color-mix(in srgb, var(--md-sys-color-primary, #0f766e) 24%, transparent);
1742
+ }
1743
+ .workspace-review:active {
1744
+ background: color-mix(in srgb, var(--md-sys-color-primary, #0f766e) 14%, transparent);
1745
+ }
1746
+ .workspace-review:focus-visible {
1747
+ outline: 2px solid var(--md-sys-color-primary, #0f766e);
1748
+ outline-offset: 1px;
1749
+ }
1750
+
1674
1751
  /* ── 접지 경고 — 근거에 없는 대상을 지목한 답 ───── */
1675
1752
  .grounding-warning {
1676
1753
  display: flex;
@@ -1979,6 +2056,115 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
1979
2056
  flex-direction: column;
1980
2057
  transition: background 0.15s, border-color 0.15s;
1981
2058
  }
2059
+ /* The wide box and a table's rows cover the conversation, inside the dock - the page beside it stays. */
2060
+ .messages.covered {
2061
+ flex: 0 0 0;
2062
+ padding: 0;
2063
+ overflow: hidden;
2064
+ visibility: hidden;
2065
+ }
2066
+ .composer.wide,
2067
+ .composer.previewing {
2068
+ flex: 1 1 auto;
2069
+ min-height: 0;
2070
+ }
2071
+ .composer.wide .input-row {
2072
+ flex: 1;
2073
+ min-height: 0;
2074
+ align-items: stretch;
2075
+ }
2076
+ .composer.wide .input-row > :not(.textarea-wrap) {
2077
+ align-self: flex-end;
2078
+ }
2079
+ .composer.wide textarea {
2080
+ min-height: 0;
2081
+ }
2082
+ .composer-size {
2083
+ margin-left: auto;
2084
+ }
2085
+ .send-shortcut {
2086
+ font-size: 10px;
2087
+ color: var(--md-sys-color-outline, #64748b);
2088
+ white-space: nowrap;
2089
+ padding-bottom: 10px;
2090
+ }
2091
+ .table-preview {
2092
+ flex: 1;
2093
+ min-height: 0;
2094
+ display: flex;
2095
+ flex-direction: column;
2096
+ margin: 4px 14px;
2097
+ border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
2098
+ border-radius: 8px;
2099
+ background: var(--md-sys-color-surface, #ffffff);
2100
+ overflow: hidden;
2101
+ }
2102
+ .table-preview-head {
2103
+ display: flex;
2104
+ align-items: center;
2105
+ gap: 6px;
2106
+ padding: 6px 8px;
2107
+ border-bottom: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
2108
+ flex-wrap: wrap;
2109
+ }
2110
+ .table-preview-head > md-icon {
2111
+ --md-icon-size: 18px;
2112
+ color: var(--md-sys-color-primary, #0284c7);
2113
+ }
2114
+ .table-preview-name {
2115
+ font-weight: 500;
2116
+ color: var(--md-sys-color-on-surface, #0f172a);
2117
+ min-width: 0;
2118
+ overflow: hidden;
2119
+ text-overflow: ellipsis;
2120
+ white-space: nowrap;
2121
+ }
2122
+ .table-preview-shape {
2123
+ color: var(--md-sys-color-on-surface-variant, #475569);
2124
+ margin-right: auto;
2125
+ }
2126
+ .table-preview-body {
2127
+ flex: 1;
2128
+ min-height: 0;
2129
+ overflow: auto;
2130
+ }
2131
+ .table-preview-body table {
2132
+ border-collapse: collapse;
2133
+ font-size: 11px;
2134
+ }
2135
+ .table-preview-body th,
2136
+ .table-preview-body td {
2137
+ border-bottom: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
2138
+ padding: 4px 8px;
2139
+ text-align: left;
2140
+ white-space: nowrap;
2141
+ color: var(--md-sys-color-on-surface, #0f172a);
2142
+ }
2143
+ .table-preview-body th {
2144
+ position: sticky;
2145
+ left: 0;
2146
+ font-weight: 400;
2147
+ text-align: right;
2148
+ color: var(--md-sys-color-on-surface-variant, #475569);
2149
+ background: var(--md-sys-color-surface-container, #f1f5f9);
2150
+ }
2151
+ .attachment-open {
2152
+ all: unset;
2153
+ display: flex;
2154
+ align-items: center;
2155
+ gap: 6px;
2156
+ min-width: 0;
2157
+ flex: 1;
2158
+ cursor: pointer;
2159
+ border-radius: 6px;
2160
+ }
2161
+ .attachment-open:focus-visible {
2162
+ outline: 2px solid var(--md-sys-color-primary, #0284c7);
2163
+ outline-offset: 2px;
2164
+ }
2165
+ .attachment-chip.open {
2166
+ border-color: var(--md-sys-color-primary, #0284c7);
2167
+ }
1982
2168
  .composer.drag-over {
1983
2169
  background: var(--md-sys-color-surface-container, #f1f5f9);
1984
2170
  border-top-color: var(--md-sys-color-primary, #0284c7);
@@ -2245,8 +2431,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2245
2431
  font: inherit;
2246
2432
  letter-spacing: inherit;
2247
2433
  resize: none;
2248
- min-height: 36px;
2249
- max-height: 140px;
2434
+ /* The resting height of one line as it stood; the box grows from here by composerHeight (shared-dock.md §10). */
2435
+ box-sizing: border-box;
2436
+ min-height: 54px;
2250
2437
  outline: none;
2251
2438
  transition:
2252
2439
  border-color 0.2s,
@@ -2366,6 +2553,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2366
2553
  this.askHostForMode(kind);
2367
2554
  }
2368
2555
  updated(changed) {
2556
+ if (changed.has('input') || changed.has('composerWide') || changed.has('previewAttachmentId') || changed.has('attachments'))
2557
+ this.fitComposer();
2369
2558
  /*
2370
2559
  * 밖에서 넣어 준 질문을 자동 전송한다(오류 토스트의 "AI에게 묻기" 등).
2371
2560
  *
@@ -2603,7 +2792,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2603
2792
  ${this.renderModeSubject()}
2604
2793
  ${this.renderSessionTabs()}
2605
2794
  ${this.renderRoster()}
2606
- <div class="messages" @click=${this.onBubbleClick}>
2795
+ <div class="messages ${this.composerWide || this.previewedTable ? 'covered' : ''}" @click=${this.onBubbleClick}>
2607
2796
  ${!hasMessages ? this.renderEmpty() : nothing}
2608
2797
  ${(() => {
2609
2798
  const myEmail = this._myEmail;
@@ -2727,7 +2916,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2727
2916
  : nothing}
2728
2917
  ${this.renderFoldNotice()}
2729
2918
  <div
2730
- class="composer ${this.dragOver ? 'drag-over' : ''}"
2919
+ class="composer ${this.dragOver ? 'drag-over' : ''} ${this.composerWide ? 'wide' : ''} ${this.previewedTable ? 'previewing' : ''}"
2731
2920
  @dragover=${(e) => this.onComposerDragOver(e)}
2732
2921
  @dragleave=${(e) => this.onComposerDragLeave(e)}
2733
2922
  @drop=${(e) => this.onComposerDrop(e)}
@@ -2779,20 +2968,43 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2779
2968
  </button>
2780
2969
  `
2781
2970
  : nothing}
2971
+ <button
2972
+ type="button"
2973
+ class="action composer-size"
2974
+ aria-pressed=${this.composerWide ? 'true' : 'false'}
2975
+ @click=${() => this.setComposerWide(!this.composerWide)}>
2976
+ <md-icon>${this.composerWide ? 'close_fullscreen' : 'open_in_full'}</md-icon>
2977
+ ${this.composerWide
2978
+ ? i18next.t('ai-assistant.button.write-narrow', { defaultValue: 'Narrow' })
2979
+ : i18next.t('ai-assistant.button.write-wide', { defaultValue: 'Write wide' })}
2980
+ </button>
2782
2981
  </div>
2982
+ ${this.renderTablePreview()}
2783
2983
  ${this.examplesOpen ? this.renderInlineExamples() : nothing}
2784
2984
  ${this.attachments.length > 0
2785
2985
  ? html `
2786
2986
  <div class="attachment-strip">
2787
2987
  ${this.attachments.map(att => html `
2788
- <div class="attachment-chip" title=${att.name}>
2789
- ${att.type.startsWith('image/')
2790
- ? html `<img class="attachment-thumb" src=${att.url} alt=${att.name} />`
2791
- : html `<md-icon class="attachment-icon">description</md-icon>`}
2792
- <div class="attachment-meta">
2793
- <span class="attachment-name">${att.name}</span>
2794
- <span class="attachment-size">${formatFileSize(att.size)}</span>
2795
- </div>
2988
+ <div class="attachment-chip ${att.id === this.previewAttachmentId ? 'open' : ''}" title=${att.name}>
2989
+ ${att.table
2990
+ ? html `<button
2991
+ type="button"
2992
+ class="attachment-open"
2993
+ aria-expanded=${att.id === this.previewAttachmentId ? 'true' : 'false'}
2994
+ @click=${() => (this.previewAttachmentId = att.id === this.previewAttachmentId ? null : att.id)}>
2995
+ <md-icon class="attachment-icon">table_chart</md-icon>
2996
+ <span class="attachment-meta">
2997
+ <span class="attachment-name">${att.name}</span>
2998
+ <span class="attachment-size">${this.tableShape(att.table)}</span>
2999
+ </span>
3000
+ </button>`
3001
+ : html `${att.type.startsWith('image/')
3002
+ ? html `<img class="attachment-thumb" src=${att.url} alt=${att.name} />`
3003
+ : html `<md-icon class="attachment-icon">description</md-icon>`}
3004
+ <div class="attachment-meta">
3005
+ <span class="attachment-name">${att.name}</span>
3006
+ <span class="attachment-size">${formatFileSize(att.size)}</span>
3007
+ </div>`}
2796
3008
  <button
2797
3009
  type="button"
2798
3010
  class="attachment-remove"
@@ -2862,10 +3074,16 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2862
3074
  </button>
2863
3075
  `
2864
3076
  : nothing}
3077
+ ${this.composerWide
3078
+ ? html `<span class="send-shortcut">${i18next.t('ai-assistant.text.send-shortcut', { key: sendShortcut(navigator.platform), defaultValue: '{key} to send' })}</span>`
3079
+ : nothing}
2865
3080
  <button
2866
3081
  ?disabled=${this.busy || !!this.sendBlocked || (!this.input.trim() && this.attachments.length === 0)}
2867
3082
  @click=${this.send}
2868
- title=${this.sendBlocked || i18next.t('ai-assistant.text.send-tooltip')}>
3083
+ title=${this.sendBlocked ||
3084
+ (this.composerWide
3085
+ ? i18next.t('ai-assistant.text.send-tooltip-wide', { key: sendShortcut(navigator.platform), defaultValue: 'Send ({key})' })
3086
+ : i18next.t('ai-assistant.text.send-tooltip'))}>
2869
3087
  ${this.busy
2870
3088
  ? html `<span class="spinner"></span>`
2871
3089
  : html `<md-icon>arrow_upward</md-icon>`}
@@ -2950,7 +3168,54 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2950
3168
  </div>
2951
3169
  `;
2952
3170
  }
3171
+ get previewedTable() {
3172
+ return this.previewAttachmentId ? this.attachments.find(a => a.id === this.previewAttachmentId && a.table) : undefined;
3173
+ }
3174
+ tableShape(table) {
3175
+ return i18next.t('ai-assistant.text.table-shape', { rows: table.rows.length, columns: table.columns, defaultValue: '{rows} rows × {columns} columns' });
3176
+ }
3177
+ /*
3178
+ The rows of a pasted table, over the conversation - numbered as the file numbers them, which is the number the
3179
+ entry card names. Every row: the paste is held under the mode's limit already.
3180
+ */
3181
+ renderTablePreview() {
3182
+ const att = this.previewedTable;
3183
+ if (!att?.table)
3184
+ return nothing;
3185
+ const { rows, columns } = att.table;
3186
+ return html `
3187
+ <div class="table-preview" role="region" aria-label=${att.name}>
3188
+ <div class="table-preview-head">
3189
+ <md-icon>table_chart</md-icon>
3190
+ <span class="table-preview-name">${att.name}</span>
3191
+ <span class="table-preview-shape">${this.tableShape(att.table)}</span>
3192
+ <button type="button" class="action" @click=${() => this.tableToWords(att.id)}>
3193
+ <md-icon>notes</md-icon>
3194
+ ${i18next.t('ai-assistant.button.table-to-words', { defaultValue: 'Put in as words' })}
3195
+ </button>
3196
+ <button type="button" class="action" @click=${() => (this.previewAttachmentId = null)}>
3197
+ <md-icon>close</md-icon>
3198
+ ${i18next.t('ai-assistant.button.close')}
3199
+ </button>
3200
+ </div>
3201
+ <div class="table-preview-body">
3202
+ <table>
3203
+ ${rows.map((row, i) => html `<tr>
3204
+ <th scope="row">${i + 1}</th>
3205
+ ${Array.from({ length: columns }, (_, c) => html `<td>${row[c] ?? ''}</td>`)}
3206
+ </tr>`)}
3207
+ </table>
3208
+ </div>
3209
+ </div>
3210
+ `;
3211
+ }
2953
3212
  onKeyDown(e) {
3213
+ /*
3214
+ The Enter that ends a Korean (or Japanese, Chinese) syllable belongs to the input method - sending on it
3215
+ sends the line without its last syllable, which then lands in the emptied box.
3216
+ */
3217
+ if (e.isComposing || e.keyCode === 229)
3218
+ return;
2954
3219
  // Mention popup 활성 시 — 화살표/Enter/Tab/Esc 가로채서 popup 네비게이션
2955
3220
  if (this.mentionOpen) {
2956
3221
  if (e.key === 'ArrowDown') {
@@ -2987,9 +3252,15 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2987
3252
  return;
2988
3253
  }
2989
3254
  }
2990
- if (e.key === 'Enter' && !e.shiftKey && !this.busy) {
3255
+ const action = composerKeyAction(e, this.composerWide);
3256
+ if (action === 'narrow') {
2991
3257
  e.preventDefault();
2992
- this.send();
3258
+ this.setComposerWide(false);
3259
+ }
3260
+ else if (action === 'send') {
3261
+ e.preventDefault();
3262
+ if (!this.busy)
3263
+ this.send();
2993
3264
  }
2994
3265
  }
2995
3266
  /** input 이벤트 — value 즉시 반영 + popup 갱신은 150ms 디바운스 (F-Finder 와 동일).
@@ -3630,7 +3901,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3630
3901
  ${entry ? html `<ox-assistant-entry-review .proposal=${p}></ox-assistant-entry-review>` : nothing}
3631
3902
  ${entry && this.sessionId && offersWorkspaceReview(p)
3632
3903
  ? html `<button
3633
- class="choice-chip"
3904
+ class="workspace-review"
3634
3905
  @click=${() => this.dispatchEvent(new CustomEvent('assistant-open-page', { detail: { href: entryReviewAddress(this.sessionId, key) }, bubbles: true, composed: true }))}
3635
3906
  >
3636
3907
  <md-icon>open_in_full</md-icon>
@@ -4156,6 +4427,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4156
4427
  if ((!text && attachmentsToSend.length === 0) || this.busy)
4157
4428
  return;
4158
4429
  this.attachments = [];
4430
+ /* What was written wide is sent to be answered - the conversation comes back to show it. */
4431
+ this.composerWide = false;
4432
+ this.previewAttachmentId = null;
4159
4433
  const promptText = text || (attachmentsToSend.length > 0
4160
4434
  ? (attachmentsToSend[0].type.startsWith('image/')
4161
4435
  ? i18next.t('ai-assistant.text.attached-image-ask', { defaultValue: 'here is an image — go by it.' })
@@ -4532,6 +4806,14 @@ __decorate([
4532
4806
  state(),
4533
4807
  __metadata("design:type", Object)
4534
4808
  ], OxAssistantChat.prototype, "dragOver", void 0);
4809
+ __decorate([
4810
+ state(),
4811
+ __metadata("design:type", Object)
4812
+ ], OxAssistantChat.prototype, "composerWide", void 0);
4813
+ __decorate([
4814
+ state(),
4815
+ __metadata("design:type", Object)
4816
+ ], OxAssistantChat.prototype, "previewAttachmentId", void 0);
4535
4817
  __decorate([
4536
4818
  state(),
4537
4819
  __metadata("design:type", Object)