Skip to content

Commit 79ff957

Browse files
authored
Merge pull request #81 from pmbstyle/1.1.9
[1.1.9] Maintenance and bug fixes
2 parents 43a4fd6 + b4ed56c commit 79ff957

File tree

7 files changed

+134
-65
lines changed

7 files changed

+134
-65
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "alice-ai-app",
3-
"version": "1.1.8",
3+
"version": "1.1.9",
44
"main": "dist-electron/main/index.js",
55
"description": "Alice AI assistant app",
66
"author": "pmbstyle",

src/App.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
class="btn btn-sm btn-primary btn-active"
3737
@click="installUpdate()"
3838
>
39-
Install & Restart
39+
<template v-if="!generalStore.isMinimized">Install & Restart</template>
40+
<template v-else>Install</template>
4041
</button>
4142
</div>
4243
</div>

src/components/Actions.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ const {
161161
openSidebar,
162162
takingScreenShot,
163163
sideBarView,
164+
isRecordingRequested,
165+
audioState: storeAudioState,
164166
} = storeToRefs(generalStore)
165167
166168
const statusMessageId = ref(
@@ -214,13 +216,13 @@ watch(isMinimized, () => {
214216
215217
const micIconSrc = computed(() => {
216218
return props.audioState === 'LISTENING' ||
217-
(generalStore.isRecordingRequested && props.audioState !== 'IDLE')
219+
(isRecordingRequested.value && props.audioState !== 'IDLE')
218220
? micIconActive
219221
: micIcon
220222
})
221223
222224
const micAriaLabel = computed(() => {
223-
return generalStore.isRecordingRequested
225+
return isRecordingRequested.value
224226
? 'Stop Microphone'
225227
: 'Start Microphone'
226228
})
@@ -280,7 +282,7 @@ const toggleMinimize = async () => {
280282
}
281283
282284
const isConfigState = computed(() => {
283-
return generalStore.audioState === 'CONFIG'
285+
return storeAudioState.value === 'CONFIG'
284286
})
285287
286288
onMounted(() => {

src/components/Main.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ import { useAudioPlayback } from '../composables/useAudioPlayback'
7474
import { useScreenshot } from '../composables/useScreenshot'
7575
import eventBus from '../utils/eventBus'
7676
77-
const { toggleRecordingRequest } = useAudioProcessing()
77+
const audioProcessing = useAudioProcessing()
78+
const { toggleRecordingRequest } = audioProcessing
7879
const { toggleTTSPreference } = useAudioPlayback()
7980
const {
8081
screenShot,

src/composables/useAudioProcessing.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { useSettingsStore } from '../stores/settingsStore'
77
import { storeToRefs } from 'pinia'
88
import eventBus from '../utils/eventBus'
99

10+
let ipcListenersRegistered = false
11+
1012
export function useAudioProcessing() {
1113
const generalStore = useGeneralStore()
1214
const conversationStore = useConversationStore()
@@ -26,17 +28,14 @@ export function useAudioProcessing() {
2628
const vadAssetBasePath = ref<string>('./')
2729

2830
const handleGlobalMicToggle = () => {
29-
console.log('[AudioProcessing] Global hotkey for mic toggle received.')
3031
toggleRecordingRequest()
3132
}
3233

3334
const handleGlobalMutePlayback = () => {
34-
console.log('[AudioProcessing] Global hotkey for mute playback received.')
3535
eventBus.emit('mute-playback-toggle')
3636
}
3737

3838
const handleGlobalTakeScreenshot = () => {
39-
console.log('[AudioProcessing] Global hotkey for take screenshot received.')
4039
eventBus.emit('take-screenshot')
4140
}
4241

@@ -82,7 +81,7 @@ export function useAudioProcessing() {
8281
)
8382
vadAssetBasePath.value = './'
8483
}
85-
if (window.ipcRenderer) {
84+
if (window.ipcRenderer && !ipcListenersRegistered) {
8685
window.ipcRenderer.on('global-hotkey-mic-toggle', handleGlobalMicToggle)
8786
window.ipcRenderer.on(
8887
'global-hotkey-mute-playback',
@@ -92,6 +91,7 @@ export function useAudioProcessing() {
9291
'global-hotkey-take-screenshot',
9392
handleGlobalTakeScreenshot
9493
)
94+
ipcListenersRegistered = true
9595
}
9696
})
9797

src/utils/markdown.ts

Lines changed: 118 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,52 +7,121 @@ const messageMarkdown = (text: string) => {
77

88
let output = text
99

10-
const markdownLinkRegex =
11-
/\[([^\]]+?)]\(\s*([^)\s]+?)(?:\s+["']([^"']+)["'])?\s*\)/g
12-
output = output.replace(markdownLinkRegex, (match, linkText, url, title) => {
13-
const href = url.trim()
14-
const titleAttribute = title ? ` title="${title.replace(/"/g, '"')}"` : ''
15-
const escapedLinkText = linkText.replace(/</g, '<').replace(/>/g, '>')
16-
return `<a href="${href}" target="_blank" rel="noopener noreferrer"${titleAttribute}>${escapedLinkText}</a>`
10+
// Process markdown links [text](url) first, before any HTML escaping
11+
const markdownLinkRegex = /\[([^\]]+)]\(([^)]+)\)/g
12+
output = output.replace(markdownLinkRegex, (match, linkText, url) => {
13+
const cleanUrl = url.trim()
14+
// Escape the link text to prevent HTML injection
15+
const escapedLinkText = linkText
16+
.replace(/&/g, '&amp;')
17+
.replace(/</g, '&lt;')
18+
.replace(/>/g, '&gt;')
19+
return `<a href="${cleanUrl}" style="text-decoration:underline;" target="_blank" rel="noopener noreferrer">${escapedLinkText}</a>`
1720
})
1821

19-
const potentialUrlRegex =
20-
/((?:https?:\/\/)?(?:[\w-]+\.)+[\w-]+(?:[\/\?#][^\s<>()"]*)?)/gi
22+
// Find existing links to avoid double-processing
23+
const existingLinksRanges: { start: number; end: number }[] = []
24+
output.replace(/<a\s[^>]*href="[^"]*"[^>]*>.*?<\/a>/gi, (linkMatch, offset) => {
25+
existingLinksRanges.push({
26+
start: offset,
27+
end: offset + linkMatch.length,
28+
})
29+
return linkMatch
30+
})
2131

32+
// Now escape HTML characters in text that is not part of links
33+
const segments: string[] = []
2234
let lastIndex = 0
23-
let processedOutput = ''
24-
let match
2535

26-
const existingLinksRanges: { start: number; end: number }[] = []
27-
output.replace(
28-
/<a\s[^>]*href="[^"]*"[^>]*>.*?<\/a>/g,
29-
(linkMatch, offset) => {
30-
existingLinksRanges.push({
31-
start: offset,
32-
end: offset + linkMatch.length,
33-
})
34-
return linkMatch
35-
}
36+
// Find all links
37+
output.replace(/<a\s[^>]*href="[^"]*"[^>]*>.*?<\/a>/gi, (linkMatch, offset) => {
38+
// Add escaped text before this link
39+
const textBefore = output.substring(lastIndex, offset)
40+
segments.push(
41+
textBefore
42+
.replace(/&/g, '&amp;')
43+
.replace(/</g, '&lt;')
44+
.replace(/>/g, '&gt;')
45+
)
46+
47+
// Add the link as-is (already processed)
48+
segments.push(linkMatch)
49+
50+
lastIndex = offset + linkMatch.length
51+
return linkMatch
52+
})
53+
54+
// Add remaining text (escaped)
55+
const remainingText = output.substring(lastIndex)
56+
segments.push(
57+
remainingText
58+
.replace(/&/g, '&amp;')
59+
.replace(/</g, '&lt;')
60+
.replace(/>/g, '&gt;')
61+
)
62+
63+
output = segments.join('')
64+
65+
// Process line breaks
66+
output = output.replace(/(\r\n|\n|\r)/gm, '<br>')
67+
68+
// Process bold (**text** or __text__)
69+
output = output.replace(/(\*\*|__)(.*?)\1/g, '<strong>$2</strong>')
70+
71+
// Process italics (*text* or _text_)
72+
output = output.replace(/(\*)(.*?)\1/g, '<em>$2</em>')
73+
74+
// Process inline code (`code`)
75+
output = output.replace(/`([^`]+)`/g, '<code>$1</code>')
76+
77+
// Process strikethrough (~~text~~)
78+
output = output.replace(/~~(.*?)~~/g, '<del>$1</del>')
79+
80+
// Process hashtags (#tag)
81+
output = output.replace(
82+
/(^|\s)#([^\s<]+)/gm,
83+
'$1<span class="hashtag">#$2</span>'
84+
)
85+
86+
// Process mentions (@user) - avoid email addresses
87+
output = output.replace(
88+
/(^|\s)@([^\s<@]+)/gm,
89+
'$1<span class="mention">@$2</span>'
3690
)
3791

92+
// Process plain URLs, but avoid email addresses and existing links
93+
// Find existing links again after all processing
94+
const finalExistingLinksRanges: { start: number; end: number }[] = []
95+
output.replace(/<a\s[^>]*href="[^"]*"[^>]*>.*?<\/a>/gi, (linkMatch, offset) => {
96+
finalExistingLinksRanges.push({
97+
start: offset,
98+
end: offset + linkMatch.length,
99+
})
100+
return linkMatch
101+
})
102+
103+
const potentialUrlRegex = /\b(?:https?:\/\/|www\.)[\w-]+(?:\.[\w-]+)+(?:[^\s<>()'"]*[^.\s<>()'"])?/gi
104+
105+
lastIndex = 0
106+
let processedOutput = ''
107+
let match
108+
38109
while ((match = potentialUrlRegex.exec(output)) !== null) {
39110
const matchStartIndex = match.index
40111
const matchEndIndex = matchStartIndex + match[0].length
41112
const urlCandidate = match[0]
42113

114+
// Check if this URL is inside an existing link
43115
let isInsideExistingLink = false
44-
for (const range of existingLinksRanges) {
116+
for (const range of finalExistingLinksRanges) {
45117
if (matchStartIndex >= range.start && matchStartIndex < range.end) {
46118
isInsideExistingLink = true
47119
break
48120
}
49121
}
50122

51-
const textBefore = output.substring(
52-
Math.max(0, matchStartIndex - 7),
53-
matchStartIndex
54-
)
55-
if (textBefore.match(/href\s*=\s*["']$/i)) {
123+
// Additional check to avoid email addresses
124+
if (urlCandidate.includes('@') && !urlCandidate.startsWith('http') && !urlCandidate.startsWith('www')) {
56125
isInsideExistingLink = true
57126
}
58127

@@ -62,41 +131,37 @@ const messageMarkdown = (text: string) => {
62131
processedOutput += urlCandidate
63132
} else {
64133
let urlToLink = urlCandidate
65-
if (
66-
!urlToLink.startsWith('http://') &&
67-
!urlToLink.startsWith('https://')
68-
) {
69-
if (/^([\w-]+\.)+[\w-]+/.test(urlToLink)) {
70-
urlToLink = `https://${urlToLink}`
71-
} else {
72-
processedOutput += urlCandidate
73-
lastIndex = matchEndIndex
74-
continue
75-
}
134+
if (!urlToLink.startsWith('http://') && !urlToLink.startsWith('https://')) {
135+
urlToLink = `https://${urlToLink}`
76136
}
77-
processedOutput += `<a href="${urlToLink}" target="_blank" rel="noopener noreferrer">${urlCandidate}</a>`
137+
processedOutput += `<a href="${urlToLink}" style="text-decoration:underline;" target="_blank" rel="noopener noreferrer">${urlCandidate}</a>`
78138
}
79139
lastIndex = matchEndIndex
80140
}
81141
processedOutput += output.substring(lastIndex)
82142
output = processedOutput
83143

84-
output = output.replace(/(\r\n|\n|\r)/gm, '<br>')
85-
output = output.replace(/(\*\*|__)(.*?)\1/g, '<strong>$2</strong>')
86-
output = output.replace(/(\*|_)(.*?)\1/g, '<em>$2</em>')
87-
output = output.replace(/`([^`]+)`/g, '<code>$1</code>')
88-
output = output.replace(/~~(.*?)~~/g, '<del>$1</del>')
144+
const linkSegments: string[] = []
145+
lastIndex = 0
89146

90-
output = output.replace(
91-
/(^|\s)#([^\s<]+)/gm,
92-
'$1<span class="hashtag">#$2</span>'
93-
)
94-
output = output.replace(
95-
/(^|\s)@([^\s<]+)/gm,
96-
'$1<span class="mention">@$2</span>'
97-
)
147+
output.replace(/<a\s[^>]*href="[^"]*"[^>]*>.*?<\/a>/gi, (linkMatch, offset) => {
148+
const textBefore = output.substring(lastIndex, offset)
149+
const processedTextBefore = textBefore.replace(/(_)_(.*?)_\1/g, '<em>$2</em>')
150+
linkSegments.push(processedTextBefore)
151+
linkSegments.push(linkMatch)
152+
153+
lastIndex = offset + linkMatch.length
154+
return linkMatch
155+
})
156+
157+
const remainingText2 = output.substring(lastIndex)
158+
159+
const processedRemainingText = remainingText2.replace(/(_)_(.*?)_\1/g, '<em>$2</em>')
160+
linkSegments.push(processedRemainingText)
161+
162+
output = linkSegments.join('')
98163

99164
return output
100165
}
101166

102-
export { messageMarkdown }
167+
export { messageMarkdown }

0 commit comments

Comments
 (0)