Skip to content

Commit b715a0f

Browse files
refactor(Messages): move helper functions to utils.ts
1 parent ff18564 commit b715a0f

2 files changed

Lines changed: 159 additions & 151 deletions

File tree

src/components/Messages/Messages.tsx

Lines changed: 7 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ import { memo } from 'react';
44

55
import { ROLE, UI } from '../../constants';
66
import type { Message as OllamaMessage } from '../../utils/ollama';
7-
import { CodeBlock, normalizeCodeBlockContent } from '../CodeBlock';
7+
import { CodeBlock } from '../CodeBlock';
88
import { Markdown } from '../Markdown';
99
import { TURN_ABORTED_MESSAGE } from './constants';
10-
import { splitStreamingInlineContent } from './utils';
10+
import {
11+
getMessageColor,
12+
parseContent,
13+
splitStreamingInlineContent,
14+
unwrapRawMarkdownFence,
15+
} from './utils';
1116

1217
interface Props {
1318
messages: OllamaMessage[];
@@ -16,155 +21,6 @@ interface Props {
1621
streamingMessage?: OllamaMessage | null;
1722
}
1823

19-
function getMessageColor(role: string): string | undefined {
20-
switch (role) {
21-
case ROLE.USER:
22-
return 'black';
23-
case ROLE.ASSISTANT:
24-
return 'cyan';
25-
case ROLE.SYSTEM:
26-
return 'gray';
27-
default:
28-
return undefined;
29-
}
30-
}
31-
32-
interface ContentSegment {
33-
type: 'text' | 'code' | 'raw';
34-
content: string;
35-
language?: string;
36-
}
37-
38-
interface FenceState {
39-
fence: string;
40-
indent: string;
41-
language?: string;
42-
rawLines: string[];
43-
ambiguous: boolean;
44-
rawFenceDepth: number;
45-
}
46-
47-
const FENCE_LINE_REGEX =
48-
/^(?<indent>[ \t]*)(?<fence>`{3,})(?<language>\w+)?[ \t]*$/;
49-
50-
function flushTextSegment(
51-
segments: ContentSegment[],
52-
textLines: string[],
53-
): void {
54-
const textContent = textLines.join('\n').trim();
55-
if (textContent) {
56-
segments.push({ type: 'text', content: textContent });
57-
}
58-
}
59-
60-
function flushCodeSegment(
61-
segments: ContentSegment[],
62-
codeLines: string[],
63-
fenceState: FenceState,
64-
): void {
65-
if (fenceState.ambiguous) {
66-
segments.push({
67-
type: 'raw',
68-
content: fenceState.rawLines.join('\n'),
69-
});
70-
return;
71-
}
72-
73-
const codeContent = normalizeCodeBlockContent(
74-
codeLines.join('\n'),
75-
fenceState.indent,
76-
);
77-
if (codeContent) {
78-
segments.push({
79-
type: 'code',
80-
content: codeContent,
81-
language: fenceState.language,
82-
});
83-
}
84-
}
85-
86-
function unwrapRawMarkdownFence(content: string): string | null {
87-
if (!content.startsWith('```markdown\n') || !content.endsWith('\n```')) {
88-
return null;
89-
}
90-
91-
return content.slice('```markdown\n'.length, -'\n```'.length);
92-
}
93-
94-
function parseContent(content: string): ContentSegment[] {
95-
const segments: ContentSegment[] = [];
96-
const lines = content.split('\n');
97-
const textLines: string[] = [];
98-
const codeLines: string[] = [];
99-
let fenceState: FenceState | null = null;
100-
101-
for (const line of lines) {
102-
const fenceMatch = FENCE_LINE_REGEX.exec(line);
103-
if (fenceMatch?.groups) {
104-
const { indent, fence, language } = fenceMatch.groups;
105-
106-
if (!fenceState) {
107-
flushTextSegment(segments, textLines);
108-
textLines.length = 0;
109-
fenceState = {
110-
indent,
111-
fence,
112-
language,
113-
rawLines: [line],
114-
ambiguous: false,
115-
rawFenceDepth: 1,
116-
};
117-
continue;
118-
}
119-
120-
if (indent === fenceState.indent && fence === fenceState.fence) {
121-
fenceState.rawLines.push(line);
122-
123-
if (fenceState.ambiguous) {
124-
if (language) {
125-
fenceState.rawFenceDepth += 1;
126-
continue;
127-
}
128-
129-
fenceState.rawFenceDepth -= 1;
130-
if (fenceState.rawFenceDepth === 0) {
131-
flushCodeSegment(segments, codeLines, fenceState);
132-
codeLines.length = 0;
133-
fenceState = null;
134-
}
135-
continue;
136-
}
137-
138-
if (!language) {
139-
flushCodeSegment(segments, codeLines, fenceState);
140-
codeLines.length = 0;
141-
fenceState = null;
142-
continue;
143-
}
144-
145-
fenceState.ambiguous = true;
146-
fenceState.rawFenceDepth += 1;
147-
continue;
148-
}
149-
}
150-
151-
if (fenceState) {
152-
fenceState.rawLines.push(line);
153-
codeLines.push(line);
154-
} else {
155-
textLines.push(line);
156-
}
157-
}
158-
159-
if (fenceState) {
160-
textLines.push(...fenceState.rawLines);
161-
}
162-
163-
flushTextSegment(segments, textLines);
164-
165-
return segments;
166-
}
167-
16824
interface MessageProps {
16925
message: OllamaMessage;
17026
isStreaming?: boolean;

src/components/Messages/utils.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,155 @@
1+
import { ROLE } from '../../constants';
2+
import { normalizeCodeBlockContent } from '../CodeBlock';
3+
4+
export interface ContentSegment {
5+
type: 'text' | 'code' | 'raw';
6+
content: string;
7+
language?: string;
8+
}
9+
10+
interface FenceState {
11+
fence: string;
12+
indent: string;
13+
language?: string;
14+
rawLines: string[];
15+
ambiguous: boolean;
16+
rawFenceDepth: number;
17+
}
18+
19+
const FENCE_LINE_REGEX =
20+
/^(?<indent>[ \t]*)(?<fence>`{3,})(?<language>\w+)?[ \t]*$/;
21+
22+
function flushTextSegment(
23+
segments: ContentSegment[],
24+
textLines: string[],
25+
): void {
26+
const textContent = textLines.join('\n').trim();
27+
if (textContent) {
28+
segments.push({ type: 'text', content: textContent });
29+
}
30+
}
31+
32+
function flushCodeSegment(
33+
segments: ContentSegment[],
34+
codeLines: string[],
35+
fenceState: FenceState,
36+
): void {
37+
if (fenceState.ambiguous) {
38+
segments.push({
39+
type: 'raw',
40+
content: fenceState.rawLines.join('\n'),
41+
});
42+
return;
43+
}
44+
45+
const codeContent = normalizeCodeBlockContent(
46+
codeLines.join('\n'),
47+
fenceState.indent,
48+
);
49+
if (codeContent) {
50+
segments.push({
51+
type: 'code',
52+
content: codeContent,
53+
language: fenceState.language,
54+
});
55+
}
56+
}
57+
58+
export function unwrapRawMarkdownFence(content: string): string | null {
59+
if (!content.startsWith('```markdown\n') || !content.endsWith('\n```')) {
60+
return null;
61+
}
62+
63+
return content.slice('```markdown\n'.length, -'\n```'.length);
64+
}
65+
66+
export function parseContent(content: string): ContentSegment[] {
67+
const segments: ContentSegment[] = [];
68+
const lines = content.split('\n');
69+
const textLines: string[] = [];
70+
const codeLines: string[] = [];
71+
let fenceState: FenceState | null = null;
72+
73+
for (const line of lines) {
74+
const fenceMatch = FENCE_LINE_REGEX.exec(line);
75+
if (fenceMatch?.groups) {
76+
const { indent, fence, language } = fenceMatch.groups;
77+
78+
if (!fenceState) {
79+
flushTextSegment(segments, textLines);
80+
textLines.length = 0;
81+
fenceState = {
82+
indent,
83+
fence,
84+
language,
85+
rawLines: [line],
86+
ambiguous: false,
87+
rawFenceDepth: 1,
88+
};
89+
continue;
90+
}
91+
92+
if (indent === fenceState.indent && fence === fenceState.fence) {
93+
fenceState.rawLines.push(line);
94+
95+
if (fenceState.ambiguous) {
96+
if (language) {
97+
fenceState.rawFenceDepth += 1;
98+
continue;
99+
}
100+
101+
fenceState.rawFenceDepth -= 1;
102+
if (fenceState.rawFenceDepth === 0) {
103+
flushCodeSegment(segments, codeLines, fenceState);
104+
codeLines.length = 0;
105+
fenceState = null;
106+
}
107+
continue;
108+
}
109+
110+
if (!language) {
111+
flushCodeSegment(segments, codeLines, fenceState);
112+
codeLines.length = 0;
113+
fenceState = null;
114+
continue;
115+
}
116+
117+
fenceState.ambiguous = true;
118+
fenceState.rawFenceDepth += 1;
119+
continue;
120+
}
121+
}
122+
123+
if (fenceState) {
124+
fenceState.rawLines.push(line);
125+
codeLines.push(line);
126+
} else {
127+
textLines.push(line);
128+
}
129+
}
130+
131+
if (fenceState) {
132+
textLines.push(...fenceState.rawLines);
133+
}
134+
135+
flushTextSegment(segments, textLines);
136+
137+
return segments;
138+
}
139+
140+
export function getMessageColor(role: string): string | undefined {
141+
switch (role) {
142+
case ROLE.USER:
143+
return 'black';
144+
case ROLE.ASSISTANT:
145+
return 'cyan';
146+
case ROLE.SYSTEM:
147+
return 'gray';
148+
default:
149+
return undefined;
150+
}
151+
}
152+
1153
function isWordCharacter(char: string | undefined): boolean {
2154
return char !== undefined && /[A-Za-z0-9]/.test(char);
3155
}

0 commit comments

Comments
 (0)