본문으로 건너뛰기

AI

AI 비서 패널 · 요약 카드 · 출처 인용 · ‘AI 생성’ 표시 · 사용량입니다. AI 가 만든 것에는 언제나 ‘AI 생성’ 표시와 출처가 붙습니다.

import
import { AssistantPanel, AISummaryCard, Citation, SourceList, UsageNotice } from "@uyoolabs/ui/ai"; import { AiLabel, AiMark, UsageMeter } from "@uyoolabs/ui/ui";
쓰는 경우
회의실 · 채널 · 문서 옆의 AI 비서(측면 패널), 회의가 끝난 뒤 · 안 읽은 메시지가 많은 채널 위의 요약 한 장, AI 가 만든 글이 그대로 보이는 모든 자리의 ‘AI 생성’ 표시, 설정 화면의 사용량과 상한 안내에 씁니다.
쓰지 않는 경우
AI 를 사람처럼 동그란 아바타 · 사람 이름으로 그리지 않습니다. 출처를 답 끝 링크 목록으로만 두지 않고 문장마다 번호를 잇습니다. 권한으로 뺀 출처의 제목 · 채널 이름을 말하지 않습니다. 오류를 답 글처럼 흘려 쓰지 않고(알림 + 다시 시도), 상한을 빨간 오류로 그리지 않습니다. ‘AI 생성’을 아이콘만으로 두지 않습니다.
서버의 일
모델 · 검색 · 권한 거르기는 제품 서버가 합니다. 컴포넌트는 거른 결과(출처 · 뺀 개수 withheldCount · 숨긴 줄 redacted)를 받아 그리기만 합니다.

AssistantPanel

묻는 말은 오른쪽 파인 칸, 답은 넓은 글입니다. 흐르는 동안에는 중지를, 끝나면 복사 · 다시 생성 · 피드백을 줍니다. 대화는 role=log 이지만 흐르는 글자는 읽지 않고 끝난 답만 한 번 읽습니다.

흐름
createStreamQueue 가 도착과 렌더를 가릅니다. 밀린 글자를 120ms 안에 비우는 속도로 흘리고, 서버가 끝나면 더 빨리 비웁니다. 출처 · 완료 같은 글자 아닌 일은 mark 로 같은 줄에 세워 앞선 글자가 나간 뒤에 실행합니다. 이모지를 반으로 자르지 않습니다.
스크롤
바닥에 붙어 있으면 따라가고, 위로 올리면 놓아 줍니다. ‘최신으로’로 돌아옵니다. 커서는 글자가 아니라 CSS 입니다.
마크다운
renderMarkdown 으로 ./markdown 을 넣습니다. ./ai 는 react-markdown 을 끌고 오지 않습니다. 넣지 않으면 문단 · 목록 · [n] 칩만 그립니다.
키동작
Enter보내기. 한 · 일 조합 중의 Enter 는 글자 확정이라 보내지 않는다
Shift+Enter줄바꿈. 8줄까지 자라고 그 뒤로는 칸 안에서 스크롤한다
Esc응답 중이면 중지(패널 어디에서나)
↑빈 묻는 칸에서 마지막 질문을 불러와 고친다
⌘⇧C패널 안에서 마지막 답을 마크다운으로 복사
Tab답의 인용 칩 → 출처 → 동작(복사 · 다시 생성 · 좋아요 · 싫어요) 순
F6앱 셸의 영역 이동으로 패널에 들어오고 나간다(패널은 aside 영역)

AISummaryCard

회의 · 채널을 출처가 달린 한 장으로 줄입니다. 요약은 상태가 아니라 내용이라 상태 색을 쓰지 않습니다. 공유받은 사람에게는 권한 없는 줄을 숨기되 숨겼다는 자리는 남깁니다.

주간 점검 요약

AI 생성회의 · 10월 22일 14:00–14:45기록 45분

요약

  • 결제 실패 알림을 이번 배포에 넣는다
  • 환불 정책 문구는 법무 검토 뒤 다음 배포로 미룬다
  • 볼 권한이 없는 출처에서 나온 줄이라 숨겼습니다

결정

  • 웹훅 서명 검증 없이는 배포하지 않는다

할 일

  • 환불 정책 문구 정리해 법무팀에 보내기
    김하늘기한 10월 27일(화)
  • 웹훅 서명 검증 구현
    최유나기한 없음

볼 권한이 없는 출처 1개는 요약에 쓰지 않았습니다.

키동작
Tab메뉴 · 인용 칩 · 할 일 체크 · ‘작업으로’ · 바닥 동작 순
Enter인용 칩: 원문 자리를 연다(기록은 Transcript 의 그 줄)
포커스 머묾인용 칩의 원문 미리보기(비모달). Esc 로 닫고 칩에 남는다
Space할 일 완료 토글
⌘C카드 안에서 아무것도 고르지 않았으면 요약 전체를 각주 마크다운으로

Citation · SourceList

문장 끝 번호 칩은 링크이고, 이름에 출처 종류 · 시각 · 사람을 담습니다(‘출처 2: 기록 14:12, 김하늘’). 미리보기는 툴팁이 아니라 비모달 Hover Card 라 포커스로도 열립니다.

환불 정책 문구는 법무 검토 뒤 다음 배포로 미룹니다 . 칩에 손을 올리거나 Tab 으로 머물면 원문이 뜨고, 누르면 그 출처를 짚습니다.

AiLabel · AiMark

‘누가 썼나’를 말하는 표시라 5톤 Badge 와 따로 둡니다. 중립 면 + 반짝 모양 + 글자이고, 끌 수 없습니다(숨기는 prop 이 없습니다). 사람이 고치면 ‘AI 생성 · ○○이 고침’, 사람이 받아 넣으면 라벨이 사라지고 기록에 남습니다.

AI 생성AI 생성AI 생성 · 출처 3AI 생성 · 이서연이 고침AI 제안

이번 달 AI 사용량

질문 · 답820 / 1,000회
회의 요약12 / 20회
기록(전사)무제한

11월 1일 00:00(한국 표준시)에 다시 채워집니다. 워크스페이스 전체가 함께 씁니다.

UsageNotice

상한은 오류가 아니라 정해진 한도라 빨강을 쓰지 않습니다. 언제 다시 쓸 수 있는지와 누구에게 물을지를 꼭 말합니다. 잠깐 너무 많음(rate)의 남은 시간은 화면에서는 1초마다, 스크린 리더에는 10초마다 알립니다.

이번 달 AI 사용량을 86% 썼습니다

860 / 1,000회 · 11월 1일에 다시 채워집니다.
요청이 많아 0:12 뒤에 다시 물을 수 있습니다

이번 달 AI 사용량을 모두 썼습니다

11월 1일에 다시 쓸 수 있습니다. 지금 필요하면 관리자에게 상한을 늘려 달라고 요청합니다.

이 워크스페이스에서는 AI 를 쓰지 않습니다

관리자(정민재)가 껐습니다.
회의 요약은 팀 요금제부터 씁니다
키동작
Tab안내의 동작(상한 늘리기 요청 · 요금제 보기) → 닫기
Esc닫을 수 있는 안내(80% 넘음)를 닫는다

props

소스의 타입과 JSDoc 에서 생성한 표입니다. 컴포넌트를 고치면 pnpm gen 으로 다시 만듭니다.

AssistantPanel

prop타입기본값설명
messages필수AssistantMessage[]—대화. 앱이 정본이다(제어만)
onSend필수(text: string, scope: string) => void | Promise<void>—보내기. Promise 가 실패하면 묻는 칸에 글을 되돌린다
onStop필수(messageId: string) => void—
onRegenerate필수(messageId: string) => void—
onContinue(messageId: string) => void—중지한 답 이어서 쓰기. 없으면 중지한 답에도 다시 생성을 둔다
onVersionChange(messageId: string, index: number) => void—
onCopy(messageId: string, markdown: string) => void—복사한 뒤. 클립보드에는 패널이 마크다운 원문을 쓴다
onFeedback(messageId: string, value: "up" | "down" | null, detail?: FeedbackDetail) => void—없으면 피드백 단추를 그리지 않는다
onOpenSource(source: Source) => void—
scope필수string—지금 범위의 id. 앱이 정본이다
scopes필수AssistantScope[]—
onScopeChange(id: string) => void—
suggestionsAssistantSuggestion[]—빈 패널의 제안 칩. 누르면 그 prompt 를 보낸다
usageAssistantUsage | null—80% 를 넘으면 대화 위에 한 번 알린다
disabledAssistantDisabled | null—관리자가 끔 · 요금제에 없음 · 상한 도달 · 잠깐 너무 많음. 묻는 칸이 비활성이 되고 이유를 잇는다
onRequestLimit() => void—
onSeePlans() => void—
onRateReady() => void—잠깐 너무 많음(rate)의 시간이 다 됐을 때. 앱이 disabled 를 푼다
draftKeystring—대화별 초안을 sessionStorage 에 남기는 열쇠. 없으면 남기지 않는다
onLongPaste(text: string) => boolean | undefined—문서 수준의 긴 붙여넣기(2,000자 이상, 또는 400자 이상이면서 제목 · 표 · 코드펜스가 보임). 첨부로 바꿀지는 앱이 정한다 — true 를 돌려주면 묻는 칸에 붙이지 않는다. 없으면 그대로 붙는다
onNewThread() => void—
onClose() => void—
titleReactNode—머리 이름. 기본 labels.title
streamStreamQueueHub | null—앱이 SSE 델타를 넣는 흐름 큐(createStreamQueue). 없으면 messages 의 content 를 그대로 그린다
renderMarkdownRenderMarkdown—답 본문 렌더. ./markdown 의 Markdown 을 넣는다. 없으면 문단 · 목록 · [n] 칩만 아는 글자 그림이다. ./ai 가 마크다운 의존성(react-markdown)을 끌고 오지 않게 주입받는다
autoFocusbooleantrue처음 그릴 때 묻는 칸에 포커스(마우스 · 트랙패드 화면에서만). 기본 true
timeZonestring—IANA 시간대. 출처 시각 · 리셋 날짜(UTC)를 이 시간대로 적는다. 기본 서울
classNamestring—
lang"ko" | "en" | "ja""ko"
labelsPartial<AssistantLabels>—
usageLabelsPartial<UsageNoticeLabels>—사용량 안내(UsageNotice)의 문구

AISummaryCard

prop타입기본값설명
kind필수"meeting" | "channel" | "thread" | "document"—
title필수ReactNode—
source필수{ label: string; startedAt?: string; endedAt?: string; size?: string }—무엇을 요약했는지. size 는 ‘기록 45분’ · ‘메시지 48개’ 처럼 앱이 적는다
sections필수SummarySection[]—
todosSummaryTodo[]—할 일. 사람이 확인하고 ‘작업으로’ 옮긴다(자동으로 만들지 않는다)
sources필수Source[]—
withheldCountnumber0권한으로 뺀 출처 수. 0 이면 문장을 그리지 않는다
status필수"generating" | "done" | "stale" | "insufficient" | "error"—
staleCountnumber0요약 뒤로 생긴 새 메시지 수(stale)
editedBy{ name: string; at: string } | null—사람이 고쳤으면. ‘AI 생성 · ○○이 고침’ 이 되고, 다시 요약하기 전에 묻는다
onRegenerate() => void—
onEdit() => void—
onSaveAsDoc() => void—
onCreateTask(todo: SummaryTodo) => void—
onTodoChange(id: string, done: boolean) => void—할 일 완료(제어). 없으면 체크박스가 읽기 전용이다
onOpenSource(source: Source) => void—
onFeedback(value: "up" | "down" | null) => void—
feedback"up" | "down" | null—
onMarkAllRead() => void—채널 요약의 ‘모두 읽음으로’
headingLevel2 | 3 | 4 | 53카드 제목의 단계(카드가 놓인 자리의 제목 수준 + 1). 단락 제목은 그 아래 단계다. 기본 3
timeZonestring—IANA 시간대. 회의 시각 · 출처 시각(UTC)을 이 시간대로 적는다. 기본 서울
classNamestring—
lang"ko" | "en" | "ja""ko"
labelsPartial<SummaryLabels>—
citationLabelsPartial<CitationLabels>—인용 칩 · 미리보기의 문구

Citation

prop타입기본값설명
timeZonestring—IANA 시간대. 출처 시각(UTC)을 이 시간대로 적는다. 기본 서울
lang"ko" | "en" | "ja""ko"
labelsPartial<CitationLabels>—
n필수number—답 · 요약 안의 번호(1부터)
source필수Source—
currentboolean—지금 짚은 출처(출처 목록 · 기록과 같은 번호가 강조된다). aria-current
onOpen(source: Source) => void—칩을 누를 때. 주면 링크 이동 대신 이것을 부른다(기록은 Transcript 의 targetId 로)
openboolean—미리보기 열림(제어). 없으면 손 올림 · 포커스로 스스로 여닫는다
onOpenChange(open: boolean) => void—
classNamestring—

SourceList

prop타입기본값설명
timeZonestring—IANA 시간대. 출처 시각(UTC)을 이 시간대로 적는다. 기본 서울
lang"ko" | "en" | "ja""ko"
labelsPartial<CitationLabels>—
sources필수Source[]—
citationsCitationRef[]—[n] → 출처. 주면 그 번호로, 없으면 목록의 자리(1부터)로 번호를 단다
withheldCountnumber—권한으로 뺀 출처 수. 0 이거나 없으면 문장을 그리지 않는다
currentIdstring—지금 짚은 출처(aria-current)
onOpen(source: Source) => void—
classNamestring—

UsageNotice

prop타입기본값설명
size"sm" | "md"—sm 은 한 줄(묻는 칸 위 · 표 위). 기본 md
timeZonestring—IANA 시간대. 리셋 날짜(UTC)를 이 시간대로 적는다. 기본 서울
idstring—비활성 묻는 칸이 aria-describedby 로 이을 id
classNamestring—
lang"ko" | "en" | "ja"—
labelsPartial<UsageNoticeLabels>—
kind필수"near" | "reached" | "rate" | "disabled" | "plan"—
usednumber—
limitnumber—
resetsAtstring—다시 채워지는 시각(UTC)
onDismiss() => void—
onRequest() => void—
canManageboolean—상한을 바꿀 수 있는 사람이면 요청 대신 바꾸기
onManage() => void—
retryAfterSecnumber—
onReady() => void—
adminstring—
planstring—
onUpgrade() => void—

AiLabel

prop타입기본값설명
kind"generated" | "suggested""generated"generated = AI 가 만든 글, suggested = 사람이 아직 받지 않은 제안(폼 칸 · 편집기). 기본 generated
variant"badge" | "text""badge"badge = 중립 면 라벨, text = 바이라인(면 없이 보조 색). 기본 badge
size"sm" | "md""md"sm 16(줄 안 · 촘촘한 목록) · md 20. 기본 md
editedBystring | null—사람이 고쳤으면 그 사람 이름. 'AI 생성 · ○○이 고침' 이 되고 라벨은 떼지 않는다
sourcesCountnumber—text 변형에서 뒤에 붙는 출처 수(‘· 출처 3’)
lang"ko" | "en" | "ja"—
labelsPartial<AiLabelLabels>—

이 밖에 <span> props(children · hidden 제외)를 그대로 받습니다.

AiMark

prop타입기본값설명
size"md" | "lg""md"md 24(대화 · 알림) · lg 32(요약 카드 머리 · 채널 메시지). 기본 md

이 밖에 <span> props(children 제외)를 그대로 받습니다.

UsageMeter

prop타입기본값설명
items필수UsageMeterItem[]—
resetsAt필수string—다시 채워지는 시각(UTC). 보는 사람의 시간대로 바꿔 시간대 이름과 함께 적는다
scope"workspace" | "user"—누가 함께 쓰는 한도인지 한 문장. 기본 없음
titleReactNode—제목. 기본 labels.title, null 이면 그리지 않는다(카드 제목이 따로 있을 때)
disabledboolean | { admin?: string }—관리자가 AI 를 껐다. 막대 대신 이유 문장
timeZonestring—IANA 시간대. 기본 서울
lang"ko" | "en" | "ja""ko"
labelsPartial<UsageMeterLabels>—

이 밖에 <div> props(title 제외)를 그대로 받습니다.