@things-factory/ai-assistant 10.1.233 → 10.1.234
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 +11 -3
- package/dist-client/components/assistant-chat.js +11 -4
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/assistant-chat-resolver.js +1 -1
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/chat-session/session-name.d.ts +8 -0
- package/dist-server/service/chat-session/session-name.js +32 -0
- package/dist-server/service/chat-session/session-name.js.map +1 -0
- package/dist-server/service/chat-session-resolver.js +24 -18
- package/dist-server/service/chat-session-resolver.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/server/service/assistant-chat-resolver.ts +1 -1
- package/server/service/chat-session/group-room.test.ts +1 -1
- package/server/service/chat-session/session-name.ts +32 -0
- package/server/service/chat-session-resolver.ts +27 -16
- package/test/refusals-in-korean.test.ts +80 -0
- package/translations/en.json +22 -2
- package/translations/ko.json +22 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/ai-assistant",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.234",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh",
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
"@operato/shell": "^10.29.39",
|
|
36
36
|
"@operato/styles": "^10.0.0",
|
|
37
37
|
"@operato/utils": "^10.29.39",
|
|
38
|
-
"@things-factory/ai-client-base": "^10.1.
|
|
39
|
-
"@things-factory/apptool-base": "^10.1.
|
|
40
|
-
"@things-factory/attachment-base": "^10.1.
|
|
41
|
-
"@things-factory/auth-base": "^10.1.
|
|
38
|
+
"@things-factory/ai-client-base": "^10.1.234",
|
|
39
|
+
"@things-factory/apptool-base": "^10.1.234",
|
|
40
|
+
"@things-factory/attachment-base": "^10.1.234",
|
|
41
|
+
"@things-factory/auth-base": "^10.1.234",
|
|
42
42
|
"@things-factory/env": "^10.1.149",
|
|
43
|
-
"@things-factory/setting-base": "^10.1.
|
|
44
|
-
"@things-factory/shell": "^10.1.
|
|
43
|
+
"@things-factory/setting-base": "^10.1.234",
|
|
44
|
+
"@things-factory/shell": "^10.1.234",
|
|
45
45
|
"dompurify": "^3.0.0",
|
|
46
46
|
"exceljs": "4.2.0",
|
|
47
47
|
"graphql-scalars": "^1.22.0",
|
|
@@ -57,5 +57,5 @@
|
|
|
57
57
|
"copyfiles": "^2.4.1",
|
|
58
58
|
"rimraf": "^5.0.0"
|
|
59
59
|
},
|
|
60
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "92f29853680fff527a5f1d167ab3ad9b27837ba9"
|
|
61
61
|
}
|
|
@@ -261,7 +261,7 @@ export class AssistantChatResolver {
|
|
|
261
261
|
let session: ChatSession | undefined
|
|
262
262
|
if (input.sessionId) {
|
|
263
263
|
const found = await sessionRepo.findOneBy({ id: input.sessionId, domain: { id: domain.id } as any })
|
|
264
|
-
if (!found) throw new Error(
|
|
264
|
+
if (!found) throw new Error(t('ai-assistant.text.session-not-found', { defaultValue: 'this conversation is not here any more. reload the list.' }))
|
|
265
265
|
/*
|
|
266
266
|
* A private conversation is written by its people only - and here more than anywhere: the answer is
|
|
267
267
|
* built on this conversation's history, so an outsider's question would read it back to them.
|
|
@@ -212,7 +212,7 @@ describe('private is the owner alone', () => {
|
|
|
212
212
|
it('only the owner changes who reads it, and only to the three it knows', async () => {
|
|
213
213
|
await doors.inviteToAISession(session.id, [guest.id], as(owner))
|
|
214
214
|
expect(await doors.setSessionVisibility(session.id, 'open', as(guest))).toBe(false)
|
|
215
|
-
await expect(doors.setSessionVisibility(session.id, 'team', as(owner))).rejects.toThrow(/
|
|
215
|
+
await expect(doors.setSessionVisibility(session.id, 'team', as(owner))).rejects.toThrow(/open, group or private/)
|
|
216
216
|
})
|
|
217
217
|
})
|
|
218
218
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Words a conversation's doors say about its name and its attachments - pure, so the rule is tested alone.
|
|
3
|
+
*
|
|
4
|
+
* A name longer than the column was cut at 200 without a word, and an empty one was answered with `false`;
|
|
5
|
+
* the person saw the old name come back and no reason (inline help audit 2026-10-30). Both are refused in words now.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type Translate = (key: string, options: any) => string
|
|
9
|
+
|
|
10
|
+
/** A session name is kept up to the column's length; a longer one is refused, not cut. */
|
|
11
|
+
export const SESSION_NAME_MAX = 200
|
|
12
|
+
|
|
13
|
+
/** The kinds a mode takes, in the reader's words - the kind ids are not words a person reads. */
|
|
14
|
+
export function attachmentKindWords(kinds: string[], t: Translate): string {
|
|
15
|
+
return kinds.map(kind => t(`ai-assistant.label.attachment-kind-${kind}`, { defaultValue: kind })).join(', ')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The name a person typed, or a refusal naming the bound. Empty means "no name" only where the door allows it. */
|
|
19
|
+
export function sessionNameOf(name: string | null | undefined, t: Translate, allowEmpty: boolean): string {
|
|
20
|
+
const trimmed = (name ?? '').trim()
|
|
21
|
+
if (!trimmed && !allowEmpty) throw new Error(t('ai-assistant.text.session-name-empty', { defaultValue: 'a conversation name cannot be empty.' }))
|
|
22
|
+
if (trimmed.length > SESSION_NAME_MAX) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
t('ai-assistant.text.session-name-too-long', {
|
|
25
|
+
max: SESSION_NAME_MAX,
|
|
26
|
+
length: trimmed.length,
|
|
27
|
+
defaultValue: 'a conversation name takes up to {max} characters; this one has {length}.'
|
|
28
|
+
})
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
return trimmed
|
|
32
|
+
}
|
|
@@ -81,6 +81,7 @@ import GraphQLUpload from 'graphql-upload/GraphQLUpload.js'
|
|
|
81
81
|
import type { FileUpload } from 'graphql-upload/GraphQLUpload.js'
|
|
82
82
|
import { getAssistantMode } from './assistant-mode/registry.js'
|
|
83
83
|
import { refuseAttachments, type AttachmentRefusal } from './chat-session/attachment-policy.js'
|
|
84
|
+
import { attachmentKindWords, sessionNameOf } from './chat-session/session-name.js'
|
|
84
85
|
import { removeAttachmentRows, saveChatAttachments } from './chat-session/session-attachments.js'
|
|
85
86
|
|
|
86
87
|
@ObjectType({
|
|
@@ -195,6 +196,10 @@ export class ChatAttachmentInfo {
|
|
|
195
196
|
size!: number
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
function sessionNotFound(context: any): Error {
|
|
200
|
+
return new Error(requestTranslator(context)('ai-assistant.text.session-not-found', { defaultValue: 'this conversation is not here any more. reload the list.' }))
|
|
201
|
+
}
|
|
202
|
+
|
|
198
203
|
/** A refusal said in the reader's words - code first, the sentence from it (ADR-0054). */
|
|
199
204
|
function attachmentRefusalMessage(refusal: AttachmentRefusal, context: any): string {
|
|
200
205
|
const t = (key: string, options: any) => (context.t ? context.t(key, options) : String(options?.defaultValue ?? key))
|
|
@@ -204,7 +209,7 @@ function attachmentRefusalMessage(refusal: AttachmentRefusal, context: any): str
|
|
|
204
209
|
case 'attachment-kind-not-accepted':
|
|
205
210
|
return t('ai-assistant.text.attachment-kind-not-accepted', {
|
|
206
211
|
name: refusal.name,
|
|
207
|
-
kinds: refusal.kinds
|
|
212
|
+
kinds: attachmentKindWords(refusal.kinds, t),
|
|
208
213
|
defaultValue: '{name} cannot be attached here - this conversation takes {kinds}.'
|
|
209
214
|
})
|
|
210
215
|
case 'attachment-too-large':
|
|
@@ -421,7 +426,7 @@ export class ChatSessionResolver {
|
|
|
421
426
|
const session = await repo.findOne({ where: { id: sessionId, domain: { id: domain.id } } as any, withDeleted: true })
|
|
422
427
|
if (!session || !(session as any).deletedAt) return false
|
|
423
428
|
if (!user?.id || (session as any).creatorId !== user.id) {
|
|
424
|
-
throw new Error('
|
|
429
|
+
throw new Error(requestTranslator(context)('ai-assistant.text.restore-creator-only', { defaultValue: 'only the person who started this conversation can bring it back.' }))
|
|
425
430
|
}
|
|
426
431
|
await repo.restore(sessionId)
|
|
427
432
|
publishSessionActivity({
|
|
@@ -520,7 +525,11 @@ export class ChatSessionResolver {
|
|
|
520
525
|
if (!session) return false
|
|
521
526
|
if (!user?.id || (session as any).creatorId !== user.id) {
|
|
522
527
|
/* 조용한 무동작이 아니라 명시 실패 — 지워진 줄 알고 넘어가면 안 된다. */
|
|
523
|
-
throw new Error(
|
|
528
|
+
throw new Error(
|
|
529
|
+
requestTranslator(context)('ai-assistant.text.delete-creator-only', {
|
|
530
|
+
defaultValue: 'only the person who started this conversation can delete it. hide it from your own list instead.'
|
|
531
|
+
})
|
|
532
|
+
)
|
|
524
533
|
}
|
|
525
534
|
await repo.softDelete(sessionId)
|
|
526
535
|
publishSessionActivity({
|
|
@@ -607,7 +616,7 @@ export class ChatSessionResolver {
|
|
|
607
616
|
): Promise<boolean> {
|
|
608
617
|
const { domain, user } = context.state
|
|
609
618
|
if (!SESSION_VISIBILITIES.includes(visibility as any)) {
|
|
610
|
-
throw new Error(
|
|
619
|
+
throw new Error(requestTranslator(context)('ai-assistant.text.visibility-unknown', { defaultValue: 'a conversation is open, group or private.' }))
|
|
611
620
|
}
|
|
612
621
|
|
|
613
622
|
const session = await getRepository(ChatSession).findOneBy({ id: sessionId, domain: { id: domain.id } as any })
|
|
@@ -657,7 +666,7 @@ export class ChatSessionResolver {
|
|
|
657
666
|
): Promise<string[]> {
|
|
658
667
|
const { domain, user } = context.state
|
|
659
668
|
const session = await getRepository(ChatSession).findOneBy({ id: sessionId, domain: { id: domain.id } as any })
|
|
660
|
-
if (!session) throw
|
|
669
|
+
if (!session) throw sessionNotFound(context)
|
|
661
670
|
|
|
662
671
|
/* Only people of this domain - an id from elsewhere is not a colleague here. */
|
|
663
672
|
const wanted = [...new Set((userIds ?? []).filter(id => typeof id === 'string' && id))].slice(0, 50)
|
|
@@ -789,8 +798,8 @@ export class ChatSessionResolver {
|
|
|
789
798
|
@Ctx() context: ResolverContext
|
|
790
799
|
): Promise<boolean> {
|
|
791
800
|
const { domain, user } = context.state
|
|
792
|
-
|
|
793
|
-
|
|
801
|
+
/* 빈 이름으로 지우지 않는다 — 지우려면 별도 의도가 필요하다. 거절은 말로 한다. */
|
|
802
|
+
const trimmed = sessionNameOf(name, requestTranslator(context), false)
|
|
794
803
|
const participant = await getRepository(ChatSessionParticipant).findOne({
|
|
795
804
|
where: { domain: { id: domain.id } as any, session: { id: sessionId } as any, user: { id: user.id } as any }
|
|
796
805
|
})
|
|
@@ -1035,10 +1044,10 @@ export class ChatSessionResolver {
|
|
|
1035
1044
|
const { domain, user, tx } = context.state
|
|
1036
1045
|
const repo = getRepository(ChatSession, tx)
|
|
1037
1046
|
const session = await repo.findOneBy({ id: sessionId, domain: { id: domain.id } })
|
|
1038
|
-
if (!session) throw
|
|
1047
|
+
if (!session) throw sessionNotFound(context)
|
|
1039
1048
|
/* The board-era rename door had no gate at all - a private conversation's name was anyone's to change. */
|
|
1040
1049
|
await writeGate(session, context, tx)
|
|
1041
|
-
await repo.update(sessionId, { name: name
|
|
1050
|
+
await repo.update(sessionId, { name: sessionNameOf(name, requestTranslator(context), true) || null, updater: user } as any)
|
|
1042
1051
|
return true
|
|
1043
1052
|
}
|
|
1044
1053
|
|
|
@@ -1056,7 +1065,7 @@ export class ChatSessionResolver {
|
|
|
1056
1065
|
): Promise<boolean> {
|
|
1057
1066
|
const { domain, user, tx } = context.state
|
|
1058
1067
|
const session = await getRepository(ChatSession, tx).findOneBy({ id: sessionId, domain: { id: domain.id } })
|
|
1059
|
-
if (!session) throw
|
|
1068
|
+
if (!session) throw sessionNotFound(context)
|
|
1060
1069
|
await writeGate(session, context, tx)
|
|
1061
1070
|
|
|
1062
1071
|
const key = (proposalKey ?? '').trim()
|
|
@@ -1105,7 +1114,7 @@ export class ChatSessionResolver {
|
|
|
1105
1114
|
): Promise<boolean> {
|
|
1106
1115
|
const { domain, user, tx } = context.state
|
|
1107
1116
|
const session = await getRepository(ChatSession, tx).findOneBy({ id: sessionId, domain: { id: domain.id } })
|
|
1108
|
-
if (!session) throw
|
|
1117
|
+
if (!session) throw sessionNotFound(context)
|
|
1109
1118
|
await writeGate(session, context, tx)
|
|
1110
1119
|
|
|
1111
1120
|
const key = (proposalKey ?? '').trim()
|
|
@@ -1154,7 +1163,7 @@ export class ChatSessionResolver {
|
|
|
1154
1163
|
): Promise<ChatAttachmentInfo[]> {
|
|
1155
1164
|
const { domain, tx } = context.state
|
|
1156
1165
|
const session = await getRepository(ChatSession, tx).findOneBy({ id: sessionId, domain: { id: domain.id } })
|
|
1157
|
-
if (!session) throw
|
|
1166
|
+
if (!session) throw sessionNotFound(context)
|
|
1158
1167
|
await writeGate(session, context, tx)
|
|
1159
1168
|
|
|
1160
1169
|
const policy = getAssistantMode((session as any).kind || kind || '')?.attachments
|
|
@@ -1186,7 +1195,7 @@ export class ChatSessionResolver {
|
|
|
1186
1195
|
): Promise<string | null> {
|
|
1187
1196
|
const { domain, user, tx } = context.state
|
|
1188
1197
|
const session = await getRepository(ChatSession, tx).findOneBy({ id: sessionId, domain: { id: domain.id } })
|
|
1189
|
-
if (!session) throw
|
|
1198
|
+
if (!session) throw sessionNotFound(context)
|
|
1190
1199
|
await writeGate(session, context, tx)
|
|
1191
1200
|
|
|
1192
1201
|
const body = roomMessageBody(content)
|
|
@@ -1196,7 +1205,9 @@ export class ChatSessionResolver {
|
|
|
1196
1205
|
const previousLast = replyToId
|
|
1197
1206
|
? await messages.findOneBy({ id: replyToId, session: { id: session.id } as any })
|
|
1198
1207
|
: await messages.findOne({ where: { session: { id: session.id } as any }, order: { createdAt: 'DESC' } })
|
|
1199
|
-
if (replyToId && !previousLast)
|
|
1208
|
+
if (replyToId && !previousLast) {
|
|
1209
|
+
throw new Error(requestTranslator(context)('ai-assistant.text.reply-target-missing', { defaultValue: 'the message you are replying to is not in this conversation.' }))
|
|
1210
|
+
}
|
|
1200
1211
|
const saved = await getRepository(ChatMessage, tx).save({
|
|
1201
1212
|
session: { id: session.id } as any,
|
|
1202
1213
|
role: 'user',
|
|
@@ -1256,7 +1267,7 @@ export class ChatSessionResolver {
|
|
|
1256
1267
|
): Promise<boolean> {
|
|
1257
1268
|
const { domain, user, tx } = context.state
|
|
1258
1269
|
const session = await getRepository(ChatSession, tx).findOneBy({ id: sessionId, domain: { id: domain.id } })
|
|
1259
|
-
if (!session) throw
|
|
1270
|
+
if (!session) throw sessionNotFound(context)
|
|
1260
1271
|
await writeGate(session, context, tx)
|
|
1261
1272
|
/* 프롬프트가 한 기록으로 폭증하지 않도록 자른다 — 맥락 전달이 목적이고 전문 보존이 아니다. */
|
|
1262
1273
|
const body = (content ?? '').trim().slice(0, 4000)
|
|
@@ -1307,7 +1318,7 @@ export class ChatSessionResolver {
|
|
|
1307
1318
|
): Promise<boolean> {
|
|
1308
1319
|
const { domain, user, tx } = context.state
|
|
1309
1320
|
const session = await getRepository(ChatSession, tx).findOneBy({ id: sessionId, domain: { id: domain.id } })
|
|
1310
|
-
if (!session) throw
|
|
1321
|
+
if (!session) throw sessionNotFound(context)
|
|
1311
1322
|
await writeGate(session, context, tx)
|
|
1312
1323
|
|
|
1313
1324
|
/* 옮겨온 대화는 **그 대화의 발언 그대로** 들어간다 — 한 덩어리 기록으로 뭉치면 사람이 읽을 수 없고
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* What this holds: **a Korean reader is refused in Korean, with the bound, and nothing is cut without a word.**
|
|
3
|
+
*
|
|
4
|
+
* Found by the inline help audit of 2026-10-30: the conversation doors threw English sentences
|
|
5
|
+
* (「ChatSession … not found」, 「Only the user who created this session…」), four owner and invite
|
|
6
|
+
* refusals had no Korean entry, the attachment refusal listed the kind ids `image, text, …` inside a
|
|
7
|
+
* Korean sentence, the roster said `3 online / 5 total` and `owner`, and a session name was cut at 200
|
|
8
|
+
* characters while an empty one came back as `false` with no reason.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, readdirSync } from 'fs'
|
|
11
|
+
import { join } from 'path'
|
|
12
|
+
|
|
13
|
+
import { attachmentKindWords, sessionNameOf, SESSION_NAME_MAX } from '../server/service/chat-session/session-name'
|
|
14
|
+
|
|
15
|
+
const ROOT = join(__dirname, '..')
|
|
16
|
+
const read = (file: string) => readFileSync(join(ROOT, file), 'utf8')
|
|
17
|
+
const ko: Record<string, string> = JSON.parse(read('translations/ko.json'))
|
|
18
|
+
|
|
19
|
+
/* The framework's single-brace placeholders, in Korean. */
|
|
20
|
+
const t = (key: string, options: any = {}) =>
|
|
21
|
+
String(ko[key] ?? options.defaultValue ?? key).replace(/\{(\w+)\}/g, (whole, name) => (name in options ? String(options[name]) : whole))
|
|
22
|
+
|
|
23
|
+
function serverFiles(dir: string): string[] {
|
|
24
|
+
return readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
|
|
25
|
+
const path = join(dir, entry.name)
|
|
26
|
+
if (entry.isDirectory()) return serverFiles(path)
|
|
27
|
+
return /\.ts$/.test(entry.name) && !/\.test\.ts$/.test(entry.name) ? [path] : []
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('a conversation door refuses in the reader’s words', () => {
|
|
32
|
+
const files = serverFiles(join(ROOT, 'server'))
|
|
33
|
+
|
|
34
|
+
it('reads the server source of this package', () => {
|
|
35
|
+
expect(files.length).toBeGreaterThanOrEqual(20)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('★ every key the server translates has a Korean entry - a missing one is read in English', () => {
|
|
39
|
+
const used = new Set(files.flatMap(file => [...readFileSync(file, 'utf8').matchAll(/\(\s*'(ai-assistant\.[a-z0-9.-]+)'/g)].map(m => m[1])))
|
|
40
|
+
expect(used.size).toBeGreaterThanOrEqual(20)
|
|
41
|
+
expect([...used].filter(key => !(key in ko))).toEqual([])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('★ the session doors throw no English sentence', () => {
|
|
45
|
+
for (const file of ['server/service/chat-session-resolver.ts', 'server/service/assistant-chat-resolver.ts']) {
|
|
46
|
+
expect({ file, english: read(file).match(/throw new Error\(\s*['`][A-Za-z][^'`]*['`]/g) ?? [] }).toEqual({ file, english: [] })
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('a session name', () => {
|
|
52
|
+
it('★ a name over the bound is refused with the bound, not cut', () => {
|
|
53
|
+
const long = '가'.repeat(SESSION_NAME_MAX + 1)
|
|
54
|
+
expect(() => sessionNameOf(long, t, false)).toThrow(`대화 이름은 ${SESSION_NAME_MAX}자까지입니다. 지금 ${SESSION_NAME_MAX + 1}자입니다.`)
|
|
55
|
+
expect(sessionNameOf(' ' + '가'.repeat(SESSION_NAME_MAX) + ' ', t, false)).toBe('가'.repeat(SESSION_NAME_MAX))
|
|
56
|
+
expect(read('server/service/chat-session-resolver.ts')).not.toMatch(/\.slice\(0, 200\)/)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('★ an empty name is refused in words where the door does not take it', () => {
|
|
60
|
+
expect(() => sessionNameOf(' ', t, false)).toThrow('대화 이름을 비울 수 없습니다.')
|
|
61
|
+
expect(sessionNameOf(' ', t, true)).toBe('')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('words on the screen', () => {
|
|
66
|
+
it('★ attachment kinds are named in Korean, on the door and on the screen', () => {
|
|
67
|
+
expect(attachmentKindWords(['image', 'text', 'spreadsheet', 'document'], t)).toBe('이미지, 텍스트 파일, 스프레드시트(엑셀·CSV), 문서(PDF·Word·PowerPoint)')
|
|
68
|
+
expect(read('server/service/chat-session-resolver.ts')).not.toMatch(/kinds\.join\(/)
|
|
69
|
+
expect(read('client/components/assistant-chat.ts')).not.toMatch(/params\.kinds = refusal\.kinds\.join\(/)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('★ the roster says online and the role in the reader’s words', () => {
|
|
73
|
+
const chat = read('client/components/assistant-chat.ts')
|
|
74
|
+
expect(chat).not.toMatch(/ online \/ |' · online'| · \$\{p\.role\}/)
|
|
75
|
+
for (const key of ['roster-count', 'participant-role-owner', 'participant-role-member', 'participant-online']) {
|
|
76
|
+
const full = key === 'roster-count' ? `ai-assistant.text.${key}` : `ai-assistant.label.${key}`
|
|
77
|
+
expect(ko[full]).toBeTruthy()
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
})
|
package/translations/en.json
CHANGED
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"ai-assistant.text.history-folded": "this conversation grew long — the earlier {count} messages were left out. a new conversation is better for a different topic.",
|
|
76
76
|
"ai-assistant.text.history-folded-summarized": "this conversation grew long — the earlier {count} messages were folded into a summary. a new conversation is better for a different topic.",
|
|
77
77
|
"ai-assistant.button.attach": "Attach",
|
|
78
|
-
"ai-assistant.tooltip.attach-file": "attach
|
|
78
|
+
"ai-assistant.tooltip.attach-file": "attach a file (drag & drop or paste supported)",
|
|
79
79
|
"ai-assistant.text.drop-files-here": "Drop images or files here",
|
|
80
80
|
"ai-assistant.button.remove-attachment": "Remove attachment",
|
|
81
81
|
"ai-assistant.session.all": "All History",
|
|
@@ -493,5 +493,25 @@
|
|
|
493
493
|
"ai-assistant.selection.region": "Work with selection",
|
|
494
494
|
"ai-assistant.selection.pick-rows": "Select rows to work on with AI.",
|
|
495
495
|
"ai-assistant.selection.save-first": "Save or cancel pending edits before working with the selected rows.",
|
|
496
|
-
"ai-assistant.selection.ask": "Read the {n} selected records from {title} and suggest what can be done with them now."
|
|
496
|
+
"ai-assistant.selection.ask": "Read the {n} selected records from {title} and suggest what can be done with them now.",
|
|
497
|
+
"ai-assistant.label.attachment-kind-image": "images",
|
|
498
|
+
"ai-assistant.label.attachment-kind-text": "text files",
|
|
499
|
+
"ai-assistant.label.attachment-kind-spreadsheet": "spreadsheets (Excel, CSV)",
|
|
500
|
+
"ai-assistant.label.attachment-kind-document": "documents (PDF, Word, PowerPoint)",
|
|
501
|
+
"ai-assistant.text.roster-count": "{online} online of {total}",
|
|
502
|
+
"ai-assistant.label.participant-role-owner": "started this conversation",
|
|
503
|
+
"ai-assistant.label.participant-role-member": "participant",
|
|
504
|
+
"ai-assistant.label.participant-online": "online",
|
|
505
|
+
"ai-assistant.text.invite-private-refused": "a private conversation is yours alone. make it a group to bring people in.",
|
|
506
|
+
"ai-assistant.text.invite-owner-only": "only the person who started this group conversation can bring people in.",
|
|
507
|
+
"ai-assistant.text.remove-owner-only": "only the person who started this conversation can send someone out.",
|
|
508
|
+
"ai-assistant.text.owner-cannot-leave": "you started this conversation, so you cannot leave it. delete it, or make it private.",
|
|
509
|
+
"ai-assistant.text.session-not-found": "this conversation is not here any more. reload the list.",
|
|
510
|
+
"ai-assistant.text.restore-creator-only": "only the person who started this conversation can bring it back.",
|
|
511
|
+
"ai-assistant.text.delete-creator-only": "only the person who started this conversation can delete it. hide it from your own list instead.",
|
|
512
|
+
"ai-assistant.text.visibility-unknown": "a conversation is open, group or private.",
|
|
513
|
+
"ai-assistant.text.reply-target-missing": "the message you are replying to is not in this conversation.",
|
|
514
|
+
"ai-assistant.text.session-name-empty": "a conversation name cannot be empty.",
|
|
515
|
+
"ai-assistant.text.session-name-too-long": "a conversation name takes up to {max} characters; this one has {length}.",
|
|
516
|
+
"ai-assistant.text.private-session-refused": "this conversation is private to the people in it - only they can write here. ask the person who started it to bring you in."
|
|
497
517
|
}
|
package/translations/ko.json
CHANGED
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"ai-assistant.text.mention-not-found": "#{token} 을(를) 찾을 수 없습니다",
|
|
76
76
|
"ai-assistant.text.session-tab-label": "세션 {n}",
|
|
77
77
|
"ai-assistant.button.attach": "첨부",
|
|
78
|
-
"ai-assistant.tooltip.attach-file": "
|
|
78
|
+
"ai-assistant.tooltip.attach-file": "파일 첨부 (클립보드 붙여넣기·드래그앤드롭 지원)",
|
|
79
79
|
"ai-assistant.text.drop-files-here": "여기에 이미지나 파일을 놓으세요",
|
|
80
80
|
"ai-assistant.button.remove-attachment": "첨부 삭제",
|
|
81
81
|
"ai-assistant.session.all": "전체 이력",
|
|
@@ -493,5 +493,25 @@
|
|
|
493
493
|
"ai-assistant.selection.region": "선택 항목 작업",
|
|
494
494
|
"ai-assistant.selection.pick-rows": "AI와 함께 작업할 행을 먼저 선택하세요.",
|
|
495
495
|
"ai-assistant.selection.save-first": "편집 중인 내용을 먼저 저장하거나 취소한 뒤 선택한 행으로 작업하세요.",
|
|
496
|
-
"ai-assistant.selection.ask": "{title}에서 선택한 {n}건을 읽고 지금 할 수 있는 일을 바로 제안해 줘."
|
|
496
|
+
"ai-assistant.selection.ask": "{title}에서 선택한 {n}건을 읽고 지금 할 수 있는 일을 바로 제안해 줘.",
|
|
497
|
+
"ai-assistant.label.attachment-kind-image": "이미지",
|
|
498
|
+
"ai-assistant.label.attachment-kind-text": "텍스트 파일",
|
|
499
|
+
"ai-assistant.label.attachment-kind-spreadsheet": "스프레드시트(엑셀·CSV)",
|
|
500
|
+
"ai-assistant.label.attachment-kind-document": "문서(PDF·Word·PowerPoint)",
|
|
501
|
+
"ai-assistant.text.roster-count": "{total}명 중 {online}명 접속 중",
|
|
502
|
+
"ai-assistant.label.participant-role-owner": "대화를 시작한 사람",
|
|
503
|
+
"ai-assistant.label.participant-role-member": "참여자",
|
|
504
|
+
"ai-assistant.label.participant-online": "접속 중",
|
|
505
|
+
"ai-assistant.text.invite-private-refused": "비공개 대화는 나만 읽습니다. 사람을 들이려면 그룹으로 바꾸세요.",
|
|
506
|
+
"ai-assistant.text.invite-owner-only": "그룹 대화에는 대화를 시작한 사람만 사람을 들일 수 있습니다.",
|
|
507
|
+
"ai-assistant.text.remove-owner-only": "대화를 시작한 사람만 참여자를 내보낼 수 있습니다.",
|
|
508
|
+
"ai-assistant.text.owner-cannot-leave": "이 대화를 시작한 사람은 나갈 수 없습니다. 대화를 지우거나 비공개로 바꾸세요.",
|
|
509
|
+
"ai-assistant.text.session-not-found": "이 대화를 찾을 수 없습니다. 목록을 새로 불러오세요.",
|
|
510
|
+
"ai-assistant.text.restore-creator-only": "대화를 시작한 사람만 되살릴 수 있습니다.",
|
|
511
|
+
"ai-assistant.text.delete-creator-only": "대화를 시작한 사람만 지울 수 있습니다. 내 목록에서 숨기기는 할 수 있습니다.",
|
|
512
|
+
"ai-assistant.text.visibility-unknown": "대화의 공개 범위는 공개·그룹·비공개 중 하나입니다.",
|
|
513
|
+
"ai-assistant.text.reply-target-missing": "답하려는 메시지가 이 대화에 없습니다.",
|
|
514
|
+
"ai-assistant.text.session-name-empty": "대화 이름을 비울 수 없습니다.",
|
|
515
|
+
"ai-assistant.text.session-name-too-long": "대화 이름은 {max}자까지입니다. 지금 {length}자입니다.",
|
|
516
|
+
"ai-assistant.text.private-session-refused": "이 대화는 참여자만 쓸 수 있습니다. 대화를 시작한 사람에게 초대를 부탁하세요."
|
|
497
517
|
}
|