Skip to content

Commit cd72e1c

Browse files
authored
fix: edit app issues (#69)
1 parent 3339d68 commit cd72e1c

8 files changed

Lines changed: 77 additions & 87 deletions

File tree

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
# SwiftChat — A Cross-platform AI Chat App
1+
# SwiftChat — A Cross-platform AI Assistant
22

3-
> 🚀 Your Personal AI AssistantFast, Private, and Easy to use
3+
> 🚀 Your Personal AI WorkspaceChat, Create Apps, and More
44
55
[![GitHub Release](https://img.shields.io/github/v/release/aws-samples/swift-chat)](https://github.com/aws-samples/swift-chat/releases)
66
[![License](https://img.shields.io/badge/license-MIT--0-green)](LICENSE)
77

88
[中文](/README_CN.md)
99

10-
SwiftChat is a fast and responsive AI chat application developed with [React Native](https://reactnative.dev/) and
10+
SwiftChat is a fast and responsive AI assistant developed with [React Native](https://reactnative.dev/) and
1111
powered by [Amazon Bedrock](https://aws.amazon.com/bedrock/), with compatibility extending to other model providers such
1212
as Ollama, DeepSeek, OpenAI and OpenAI Compatible. With its minimalist design philosophy and robust privacy protection,
13-
it delivers real-time streaming conversations, AI image generation and voice conversation assistant capabilities
14-
across Android, iOS, and macOS platforms.
13+
it delivers real-time streaming conversations, AI image generation, instant web app creation and voice conversation
14+
capabilities across Android, iOS, and macOS platforms.
1515

1616
![](assets/promo.avif)
1717

@@ -32,7 +32,7 @@ across Android, iOS, and macOS platforms.
3232
<img src="assets/animations/share_and_import.avif" width=24%>
3333
</div>
3434

35-
**App Examples**: 2048 Game, Gomoku, News Reader and Tetris
35+
**App Examples**: 2048 Game, Gomoku, Tetris and News Reader
3636

3737
<div style="display: flex; flex-direction: 'row'; background-color: #888888;">
3838
<img src="assets/animations/app_2048.avif" width=24%>
@@ -245,6 +245,8 @@ can enable the **Use Proxy** option to forward your requests.
245245
## Key Features
246246

247247
- Real-time streaming chat with AI
248+
- Instant web app creation, editing and sharing
249+
- Web search for real-time information retrieval
248250
- Rich Markdown Support: Tables, Code Blocks, LaTeX, Mermaid Chart and More
249251
- AI image generation with progress
250252
- Multimodal support (images, videos & documents)

README_CN.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
# SwiftChat — 跨平台 AI 聊天应用
1+
# SwiftChat — 跨平台 AI 助手
22

3-
> 🚀 您的个人 AI 助手快速、私有、易于使用
3+
> 🚀 您的个人 AI 工作空间聊天、创建应用等
44
55
[![GitHub Release](https://img.shields.io/github/v/release/aws-samples/swift-chat)](https://github.com/aws-samples/swift-chat/releases)
66
[![License](https://img.shields.io/badge/license-MIT--0-green)](LICENSE)
77

88
[English](/README.md)
99

10-
SwiftChat 是一款快速响应的 AI 聊天应用,采用 [React Native](https://reactnative.dev/)
10+
SwiftChat 是一款快速响应的 AI 助手,采用 [React Native](https://reactnative.dev/)
1111
开发,并依托 [Amazon Bedrock](https://aws.amazon.com/bedrock/) 提供强大支持,同时兼容 Ollama、DeepSeek、OpenAI 和 OpenAI
1212
兼容的其他模型供应商。凭借其极简设计理念与坚实的隐私保护措施,该应用在 Android、iOS 和 macOS 平台上实现了实时流式对话、AI
13-
图像生成和语音对话助手功能
13+
图像生成、极速 Web 应用创建和语音对话功能
1414

1515
![](assets/promo.avif)
1616

@@ -31,7 +31,7 @@ SwiftChat 是一款快速响应的 AI 聊天应用,采用 [React Native](https
3131
<img src="assets/animations/share_and_import.avif" width=24%>
3232
</div>
3333

34-
**应用示例**:2048 游戏、五子棋、新闻阅读器和俄罗斯方块
34+
**应用示例**:2048 游戏、五子棋、俄罗斯方块和新闻阅读器
3535

3636
<div style="display: flex; flex-direction: 'row'; background-color: #888888;">
3737
<img src="assets/animations/app_2048.avif" width=24%>
@@ -234,7 +234,9 @@ SwiftChat 是一款快速响应的 AI 聊天应用,采用 [React Native](https
234234
## 主要功能
235235

236236
- 与 AI 进行实时流式聊天
237-
- 丰富的 Markdown 支持:表格、代码块、LaTeX, Mermaid图标等
237+
- 极速 Web 应用创建、编辑和分享
238+
- 网络搜索实时信息检索
239+
- 丰富的 Markdown 支持:表格、代码块、LaTeX、Mermaid 图表等
238240
- 带进度显示的 AI 图像生成
239241
- 多模态支持(图像、视频和文档)
240242
- 对话历史列表查看和管理

react-native/src/app/AppGalleryScreen.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ function AppGalleryScreen(): React.JSX.Element {
363363
navigation.navigate('Bedrock', {
364364
editAppCode: app.htmlCode,
365365
editAppName: app.name,
366+
editTimestamp: Date.now(),
366367
});
367368
}
368369
}

react-native/src/chat/ChatScreen.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ const findLatestHtmlCode = (messages: SwiftChatMessage[]): string => {
102102
return '';
103103
};
104104

105+
/**
106+
* Check if any AI message has diffCode (for detecting failed diff apply case)
107+
*/
108+
const hasAnyDiffCode = (messages: SwiftChatMessage[]): boolean => {
109+
const aiMessages = messages.filter(m => m.user._id === BOT_ID);
110+
return aiMessages.some(msg => msg.diffCode);
111+
};
112+
105113
const createBotMessage = (mode: string, isAppMode: boolean = false) => {
106114
return {
107115
_id: uuid.v4(),
@@ -132,6 +140,7 @@ function ChatScreen(): React.JSX.Element {
132140
const mode = route.params?.mode ?? currentMode;
133141
const editAppCode = route.params?.editAppCode;
134142
const editAppName = route.params?.editAppName;
143+
const editTimestamp = route.params?.editTimestamp;
135144
const modeRef = useRef(mode);
136145
const isNovaSonic =
137146
getTextModel().modelId.includes('sonic') &&
@@ -261,6 +270,7 @@ function ChatScreen(): React.JSX.Element {
261270
setMessages([]);
262271
bedrockMessages.current = [];
263272
clearLatestHtmlCode();
273+
setUsage(undefined);
264274
showKeyboard();
265275
}, [])
266276
);
@@ -354,8 +364,7 @@ function ChatScreen(): React.JSX.Element {
354364
const restoredHtmlCode = findLatestHtmlCode(msg as SwiftChatMessage[]);
355365
setLatestHtmlCode(restoredHtmlCode);
356366

357-
// If session has htmlCode, auto-select App prompt
358-
if (restoredHtmlCode) {
367+
if (restoredHtmlCode || hasAnyDiffCode(msg as SwiftChatMessage[])) {
359368
isAppModeRef.current = true;
360369
sendEventRef.current?.('selectAppPrompt');
361370
} else {
@@ -386,8 +395,9 @@ function ChatScreen(): React.JSX.Element {
386395

387396
// editAppCode handler - for editing saved apps from AppGallery
388397
useEffect(() => {
389-
if (editAppCode) {
398+
if (editAppCode && editTimestamp) {
390399
startNewChat.current();
400+
setUsage(undefined);
391401
setLatestHtmlCode(editAppCode);
392402
isAppModeRef.current = true;
393403
setTimeout(() => {
@@ -401,7 +411,7 @@ function ChatScreen(): React.JSX.Element {
401411
}
402412
}, 100);
403413
}
404-
}, [editAppCode, editAppName, navigation]);
414+
}, [editAppCode, editAppName, editTimestamp]);
405415

406416
// deleteChat listener
407417
useEffect(() => {
@@ -441,7 +451,7 @@ function ChatScreen(): React.JSX.Element {
441451
// diffApplied listener for App mode - update message.htmlCode and save diffCode
442452
// Note: placeholder replacement is done in ChatStatus.Complete handler
443453
useEffect(() => {
444-
if (event?.event === 'diffApplied' && event.params?.htmlCode) {
454+
if (event?.event === 'diffApplied' && event.params?.diffCode) {
445455
const { htmlCode, diffCode } = event.params;
446456
setMessages(prevMessages => {
447457
// update isLastHtml for all messages
@@ -520,7 +530,7 @@ function ChatScreen(): React.JSX.Element {
520530
if (isAppModeRef.current && msg.htmlCode) {
521531
msg.text = replaceHtmlWithPlaceholder(msg.text, msg.htmlCode);
522532
}
523-
if (isAppModeRef.current && msg.diffCode) {
533+
if (isAppModeRef.current && msg.diffCode && msg.htmlCode) {
524534
msg.text = replaceDiffWithPlaceholder(msg.text, msg.diffCode);
525535
}
526536
saveCurrentMessages();

react-native/src/chat/component/markdown/HtmlCodeRenderer.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const HtmlCodeRenderer = forwardRef<HtmlCodeRendererRef, HtmlCodeRendererProps>(
8484
undefined
8585
);
8686
const [hasLoadedCode, setHasLoadedCode] = useState(
87-
() => !messageHtmlCode && !isCompleted
87+
() => (!messageHtmlCode && !isCompleted) || Boolean(messageDiffCode)
8888
);
8989
const [webViewLoaded, setWebViewLoaded] = useState(false);
9090
const htmlRendererRef = useRef<HtmlPreviewRendererRef>(null);
@@ -134,6 +134,7 @@ const HtmlCodeRenderer = forwardRef<HtmlCodeRendererRef, HtmlCodeRendererProps>(
134134
setShowPreview(true);
135135
sendEvent('diffApplied', { htmlCode: result, diffCode: text });
136136
} else {
137+
sendEvent('diffApplied', { htmlCode: undefined, diffCode: text });
137138
showInfo('Diff apply failed, please regenerate');
138139
}
139140
}

react-native/src/chat/util/ApplyDiff.ts

Lines changed: 40 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -287,37 +287,6 @@ function findAllMatches(
287287
return positions;
288288
}
289289

290-
/** Get the longest line from an array (most likely to be unique) */
291-
function getLongestLine(lines: string[]): string | null {
292-
if (lines.length === 0) {
293-
return null;
294-
}
295-
296-
let longest = lines[0];
297-
for (const line of lines) {
298-
if (line.trim().length > longest.trim().length) {
299-
longest = line;
300-
}
301-
}
302-
return longest.trim().length > 0 ? longest : null;
303-
}
304-
305-
/** Find line position in source */
306-
function findLinePosition(
307-
sourceLines: string[],
308-
line: string,
309-
startFrom: number = 0
310-
): number[] {
311-
const positions: number[] = [];
312-
const trimmedLine = line.trim();
313-
for (let i = startFrom; i < sourceLines.length; i++) {
314-
if (sourceLines[i].trim() === trimmedLine) {
315-
positions.push(i);
316-
}
317-
}
318-
return positions;
319-
}
320-
321290
/**
322291
* Find block position using three-layer fallback strategy
323292
*/
@@ -388,7 +357,10 @@ function findBlockPosition(
388357
}
389358

390359
// Last resort: removals only
391-
if (removals.length >= 2) {
360+
if (
361+
removals.length >= 2 ||
362+
(removals.length === 1 && removals[0].trim().length >= 5)
363+
) {
392364
pos = trimmedMatch(sourceLines, removals, startFrom);
393365
if (pos !== -1) {
394366
return pos;
@@ -576,44 +548,31 @@ export function applyDiff(
576548
} else if (allPositions.length > 1) {
577549
// Multiple matches - try to disambiguate using middle context
578550
if (firstMiddleContext.length > 0) {
579-
const longestMiddleLine = getLongestLine(firstMiddleContext);
551+
// Sort candidates to prefer those after lastBlockEnd
552+
const sortedCandidates = [...allPositions].sort((a, b) => {
553+
const aAfter =
554+
a + effectiveContext.length >= lastBlockEnd ? 0 : 1;
555+
const bAfter =
556+
b + effectiveContext.length >= lastBlockEnd ? 0 : 1;
557+
return aAfter - bAfter || a - b;
558+
});
559+
560+
for (const candidatePos of sortedCandidates) {
561+
const changePos = candidatePos + effectiveContext.length;
562+
const searchAfter = changePos + removals.length;
580563

581-
if (longestMiddleLine) {
582-
// Find positions of the longest middle line
583-
const middleLinePositions = findLinePosition(
564+
// Check if middle context matches after this candidate
565+
const middlePositions = findAllMatches(
584566
sourceLines,
585-
longestMiddleLine,
586-
0
567+
firstMiddleContext,
568+
searchAfter
587569
);
588-
589-
// For each candidate position, check if middle line appears after it
590-
// Sort candidates to prefer those after lastBlockEnd
591-
const sortedCandidates = [...allPositions].sort((a, b) => {
592-
const aAfter =
593-
a + effectiveContext.length >= lastBlockEnd ? 0 : 1;
594-
const bAfter =
595-
b + effectiveContext.length >= lastBlockEnd ? 0 : 1;
596-
return aAfter - bAfter || a - b;
597-
});
598-
599-
for (const candidatePos of sortedCandidates) {
600-
const changePos = candidatePos + effectiveContext.length;
601-
const searchAfter = changePos + removals.length;
602-
603-
// Check if any middle line position is reasonably close after this candidate
604-
for (const middlePos of middleLinePositions) {
605-
if (
606-
middlePos >= searchAfter &&
607-
middlePos < searchAfter + 100
608-
) {
609-
// Found: this candidate has the middle context after it
610-
position = changePos;
611-
break;
612-
}
613-
}
614-
if (position !== -1) {
615-
break;
616-
}
570+
if (
571+
middlePositions.length > 0 &&
572+
middlePositions[0] < searchAfter + 100
573+
) {
574+
position = changePos;
575+
break;
617576
}
618577
}
619578
}
@@ -641,6 +600,20 @@ export function applyDiff(
641600
position = findBlockPosition(sourceLines, block, lastBlockEnd);
642601
}
643602

603+
// Fallback: use firstMiddleContext to reverse-locate position
604+
// When context is wrong but middle context exists, find middle context position
605+
// and calculate insertion point by going back removals.length lines
606+
if (position === -1 && firstMiddleContext.length > 0) {
607+
const middlePositions = findAllMatches(
608+
sourceLines,
609+
firstMiddleContext,
610+
lastBlockEnd
611+
);
612+
if (middlePositions.length >= 1) {
613+
position = middlePositions[0] - removals.length;
614+
}
615+
}
616+
644617
if (position === -1) {
645618
const context = block.contextBefore.slice(0, 2).join('\n');
646619
const removal = block.removals.slice(0, 2).join('\n');

react-native/src/chat/util/DiffUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
export const HTML_CODE_PLACEHOLDER = '[HTML_OUTPUT_OMITTED]';
33

44
// Placeholder for diff code block - diff has been applied, no need to send again
5-
export const DIFF_CODE_PLACEHOLDER = '[DIFF_CHANGES_OMITTED]';
5+
export const DIFF_CODE_PLACEHOLDER = '[PREVIOUS_DIFF_APPLIED]';
66

77
// Global storage for latest HTML code in App mode
88
let latestHtmlCode: string = '';

react-native/src/types/RouteTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export type RouteParamList = {
2020
mode?: ChatMode;
2121
editAppCode?: string;
2222
editAppName?: string;
23+
editTimestamp?: number;
2324
};
2425
Settings: NonNullable<unknown>;
2526
TokenUsage: NonNullable<unknown>;

0 commit comments

Comments
 (0)