chore: app -> desktop

This commit is contained in:
Adam
2025-10-03 09:04:28 -05:00
parent 1d58b55482
commit 3fa280d218
1143 changed files with 50 additions and 50 deletions

View File

@@ -0,0 +1,41 @@
export namespace Binary {
export function search<T>(array: T[], id: string, compare: (item: T) => string): { found: boolean; index: number } {
let left = 0
let right = array.length - 1
while (left <= right) {
const mid = Math.floor((left + right) / 2)
const midId = compare(array[mid])
if (midId === id) {
return { found: true, index: mid }
} else if (midId < id) {
left = mid + 1
} else {
right = mid - 1
}
}
return { found: false, index: left }
}
export function insert<T>(array: T[], item: T, compare: (item: T) => string): T[] {
const id = compare(item)
let left = 0
let right = array.length
while (left < right) {
const mid = Math.floor((left + right) / 2)
const midId = compare(array[mid])
if (midId < id) {
left = mid + 1
} else {
right = mid
}
}
array.splice(left, 0, item)
return array
}
}

View File

@@ -0,0 +1,51 @@
export function getCharacterOffsetInLine(lineElement: Element, targetNode: Node, offset: number): number {
const r = document.createRange()
r.selectNodeContents(lineElement)
r.setEnd(targetNode, offset)
return r.toString().length
}
export function getNodeOffsetInLine(lineElement: Element, charIndex: number): { node: Node; offset: number } | null {
const walker = document.createTreeWalker(lineElement, NodeFilter.SHOW_TEXT, null)
let remaining = Math.max(0, charIndex)
let lastText: Node | null = null
let lastLen = 0
let node: Node | null
while ((node = walker.nextNode())) {
const len = node.textContent?.length || 0
lastText = node
lastLen = len
if (remaining <= len) return { node, offset: remaining }
remaining -= len
}
if (lastText) return { node: lastText, offset: lastLen }
if (lineElement.firstChild) return { node: lineElement.firstChild, offset: 0 }
return null
}
export function getSelectionInContainer(
container: HTMLElement,
): { sl: number; sch: number; el: number; ech: number } | null {
const s = window.getSelection()
if (!s || s.rangeCount === 0) return null
const r = s.getRangeAt(0)
const sc = r.startContainer
const ec = r.endContainer
const getLineElement = (n: Node) =>
(n.nodeType === Node.TEXT_NODE ? (n.parentElement as Element) : (n as Element))?.closest(".line")
const sle = getLineElement(sc)
const ele = getLineElement(ec)
if (!sle || !ele) return null
if (!container.contains(sle as Node) || !container.contains(ele as Node)) return null
const cc = container.querySelector("code") as HTMLElement | null
if (!cc) return null
const lines = Array.from(cc.querySelectorAll(".line"))
const sli = lines.indexOf(sle as Element)
const eli = lines.indexOf(ele as Element)
if (sli === -1 || eli === -1) return null
const sl = sli + 1
const el = eli + 1
const sch = getCharacterOffsetInLine(sle as Element, sc, r.startOffset)
const ech = getCharacterOffsetInLine(ele as Element, ec, r.endOffset)
return { sl, sch, el, ech }
}

View File

@@ -0,0 +1,2 @@
export * from "./path"
export * from "./dom"

View File

@@ -0,0 +1,14 @@
export function getFilename(path: string) {
const parts = path.split("/")
return parts[parts.length - 1]
}
export function getDirectory(path: string) {
const parts = path.split("/")
return parts.slice(0, parts.length - 1).join("/")
}
export function getFileExtension(path: string) {
const parts = path.split(".")
return parts[parts.length - 1]
}

View File

@@ -0,0 +1,302 @@
import { createSignal, onCleanup } from "solid-js"
// Minimal types to avoid relying on non-standard DOM typings
type RecognitionResult = {
0: { transcript: string }
isFinal: boolean
}
type RecognitionEvent = {
results: RecognitionResult[]
resultIndex: number
}
interface Recognition {
continuous: boolean
interimResults: boolean
lang: string
start: () => void
stop: () => void
onresult: ((e: RecognitionEvent) => void) | null
onerror: ((e: { error: string }) => void) | null
onend: (() => void) | null
onstart: (() => void) | null
}
const COMMIT_DELAY = 250
const appendSegment = (base: string, addition: string) => {
const trimmed = addition.trim()
if (!trimmed) return base
if (!base) return trimmed
const needsSpace = /\S$/.test(base) && !/^[,.;!?]/.test(trimmed)
return `${base}${needsSpace ? " " : ""}${trimmed}`
}
const extractSuffix = (committed: string, hypothesis: string) => {
const cleanHypothesis = hypothesis.trim()
if (!cleanHypothesis) return ""
const baseTokens = committed.trim() ? committed.trim().split(/\s+/) : []
const hypothesisTokens = cleanHypothesis.split(/\s+/)
let index = 0
while (
index < baseTokens.length &&
index < hypothesisTokens.length &&
baseTokens[index] === hypothesisTokens[index]
) {
index += 1
}
if (index < baseTokens.length) return ""
return hypothesisTokens.slice(index).join(" ")
}
export function createSpeechRecognition(opts?: {
lang?: string
onFinal?: (text: string) => void
onInterim?: (text: string) => void
}) {
const hasSupport =
typeof window !== "undefined" &&
Boolean((window as any).webkitSpeechRecognition || (window as any).SpeechRecognition)
const [isRecording, setIsRecording] = createSignal(false)
const [committed, setCommitted] = createSignal("")
const [interim, setInterim] = createSignal("")
let recognition: Recognition | undefined
let shouldContinue = false
let committedText = ""
let sessionCommitted = ""
let pendingHypothesis = ""
let lastInterimSuffix = ""
let shrinkCandidate: string | undefined
let commitTimer: number | undefined
const cancelPendingCommit = () => {
if (commitTimer === undefined) return
clearTimeout(commitTimer)
commitTimer = undefined
}
const commitSegment = (segment: string) => {
const nextCommitted = appendSegment(committedText, segment)
if (nextCommitted === committedText) return
committedText = nextCommitted
setCommitted(committedText)
if (opts?.onFinal) opts.onFinal(segment.trim())
}
const promotePending = () => {
if (!pendingHypothesis) return
const suffix = extractSuffix(sessionCommitted, pendingHypothesis)
if (!suffix) {
pendingHypothesis = ""
return
}
sessionCommitted = appendSegment(sessionCommitted, suffix)
commitSegment(suffix)
pendingHypothesis = ""
lastInterimSuffix = ""
shrinkCandidate = undefined
setInterim("")
if (opts?.onInterim) opts.onInterim("")
}
const applyInterim = (suffix: string, hypothesis: string) => {
cancelPendingCommit()
pendingHypothesis = hypothesis
lastInterimSuffix = suffix
shrinkCandidate = undefined
setInterim(suffix)
if (opts?.onInterim) {
opts.onInterim(suffix ? appendSegment(committedText, suffix) : "")
}
if (!suffix) return
const snapshot = hypothesis
commitTimer = window.setTimeout(() => {
if (pendingHypothesis !== snapshot) return
const currentSuffix = extractSuffix(sessionCommitted, pendingHypothesis)
if (!currentSuffix) return
sessionCommitted = appendSegment(sessionCommitted, currentSuffix)
commitSegment(currentSuffix)
pendingHypothesis = ""
lastInterimSuffix = ""
shrinkCandidate = undefined
setInterim("")
if (opts?.onInterim) opts.onInterim("")
}, COMMIT_DELAY)
}
if (hasSupport) {
const Ctor: new () => Recognition = (window as any).webkitSpeechRecognition || (window as any).SpeechRecognition
recognition = new Ctor()
recognition.continuous = false
recognition.interimResults = true
recognition.lang = opts?.lang || (typeof navigator !== "undefined" ? navigator.language : "en-US")
recognition.onresult = (event: RecognitionEvent) => {
if (!event.results.length) return
let aggregatedFinal = ""
let latestHypothesis = ""
for (let i = 0; i < event.results.length; i += 1) {
const result = event.results[i]
const transcript = (result[0]?.transcript || "").trim()
if (!transcript) continue
if (result.isFinal) {
aggregatedFinal = appendSegment(aggregatedFinal, transcript)
} else {
latestHypothesis = transcript
}
}
if (aggregatedFinal) {
cancelPendingCommit()
const finalSuffix = extractSuffix(sessionCommitted, aggregatedFinal)
if (finalSuffix) {
sessionCommitted = appendSegment(sessionCommitted, finalSuffix)
commitSegment(finalSuffix)
}
pendingHypothesis = ""
lastInterimSuffix = ""
shrinkCandidate = undefined
setInterim("")
if (opts?.onInterim) opts.onInterim("")
return
}
cancelPendingCommit()
if (!latestHypothesis) {
shrinkCandidate = undefined
applyInterim("", "")
return
}
const suffix = extractSuffix(sessionCommitted, latestHypothesis)
if (!suffix) {
if (!lastInterimSuffix) {
shrinkCandidate = undefined
applyInterim("", latestHypothesis)
return
}
if (shrinkCandidate === "") {
applyInterim("", latestHypothesis)
return
}
shrinkCandidate = ""
pendingHypothesis = latestHypothesis
return
}
if (lastInterimSuffix && suffix.length < lastInterimSuffix.length) {
if (shrinkCandidate === suffix) {
applyInterim(suffix, latestHypothesis)
return
}
shrinkCandidate = suffix
pendingHypothesis = latestHypothesis
return
}
shrinkCandidate = undefined
applyInterim(suffix, latestHypothesis)
}
recognition.onerror = (e: { error: string }) => {
cancelPendingCommit()
lastInterimSuffix = ""
shrinkCandidate = undefined
if (e.error === "no-speech" && shouldContinue) {
setInterim("")
if (opts?.onInterim) opts.onInterim("")
setTimeout(() => {
try {
recognition?.start()
} catch {}
}, 150)
return
}
shouldContinue = false
setIsRecording(false)
}
recognition.onstart = () => {
sessionCommitted = ""
pendingHypothesis = ""
cancelPendingCommit()
lastInterimSuffix = ""
shrinkCandidate = undefined
setInterim("")
if (opts?.onInterim) opts.onInterim("")
setIsRecording(true)
}
recognition.onend = () => {
cancelPendingCommit()
lastInterimSuffix = ""
shrinkCandidate = undefined
setIsRecording(false)
if (shouldContinue) {
setTimeout(() => {
try {
recognition?.start()
} catch {}
}, 150)
}
}
}
const start = () => {
if (!recognition) return
shouldContinue = true
sessionCommitted = ""
pendingHypothesis = ""
cancelPendingCommit()
lastInterimSuffix = ""
shrinkCandidate = undefined
setInterim("")
try {
recognition.start()
} catch {}
}
const stop = () => {
if (!recognition) return
shouldContinue = false
promotePending()
cancelPendingCommit()
lastInterimSuffix = ""
shrinkCandidate = undefined
setInterim("")
if (opts?.onInterim) opts.onInterim("")
try {
recognition.stop()
} catch {}
}
onCleanup(() => {
shouldContinue = false
promotePending()
cancelPendingCommit()
lastInterimSuffix = ""
shrinkCandidate = undefined
setInterim("")
if (opts?.onInterim) opts.onInterim("")
try {
recognition?.stop()
} catch {}
})
return {
isSupported: () => hasSupport,
isRecording,
committed,
interim,
start,
stop,
}
}