diff --git a/packages/app/src/components/chat/ChatMessage.vue b/packages/app/src/components/chat/ChatMessage.vue index 0b4d2d29..071ca4e7 100644 --- a/packages/app/src/components/chat/ChatMessage.vue +++ b/packages/app/src/components/chat/ChatMessage.vue @@ -223,6 +223,14 @@ /> +
+ +
+
{ return extractRunnableCodeBlocks(props.message.content) }) +const bitcoinAddresses = computed(() => { + if (isUser.value) return [] + return detectBitcoinAddresses(props.message.content) +}) + const isCodeResponse = computed(() => !isUser.value && props.triggeringQuery.trim().toLowerCase() === '/code' ) diff --git a/packages/app/src/components/renderers/BitcoinAddressCard.vue b/packages/app/src/components/renderers/BitcoinAddressCard.vue new file mode 100644 index 00000000..a8686f8d --- /dev/null +++ b/packages/app/src/components/renderers/BitcoinAddressCard.vue @@ -0,0 +1,95 @@ + + + diff --git a/packages/app/src/composables/useBitcoinDetector.ts b/packages/app/src/composables/useBitcoinDetector.ts new file mode 100644 index 00000000..20e5c1b0 --- /dev/null +++ b/packages/app/src/composables/useBitcoinDetector.ts @@ -0,0 +1,114 @@ +export interface DetectedBitcoinAddress { + address: string + type: 'bech32' | 'bech32m' | 'p2pkh' | 'p2sh' | 'testnet' +} + +export interface DetectedLightningInvoice { + invoice: string + amount?: number + description?: string +} + +export interface DetectedBolt12Offer { + offer: string +} + +export interface DetectedTxId { + txid: string +} + +// Bitcoin address patterns +const BECH32_REGEX = /\b(bc1q[a-z0-9]{38,62})\b/gi +const BECH32M_REGEX = /\b(bc1p[a-z0-9]{38,62})\b/gi +const P2PKH_REGEX = /\b(1[13456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{25,34})\b/g +const P2SH_REGEX = /\b(3[13456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{25,34})\b/g +const TESTNET_REGEX = /\b(tb1[a-z0-9]{38,62})\b/gi + +// Lightning patterns +const BOLT11_REGEX = /\b(lnbc[a-z0-9]+)\b/gi +const BOLT12_REGEX = /\b(lno1[a-z0-9]+)\b/gi + +// Transaction ID (64 hex chars) +const TXID_REGEX = /\b([a-f0-9]{64})\b/gi + +export function detectBitcoinAddresses(text: string): DetectedBitcoinAddress[] { + const addresses: DetectedBitcoinAddress[] = [] + const seen = new Set() + + for (const match of text.matchAll(BECH32_REGEX)) { + if (!seen.has(match[1].toLowerCase())) { + seen.add(match[1].toLowerCase()) + addresses.push({ address: match[1], type: 'bech32' }) + } + } + for (const match of text.matchAll(BECH32M_REGEX)) { + if (!seen.has(match[1].toLowerCase())) { + seen.add(match[1].toLowerCase()) + addresses.push({ address: match[1], type: 'bech32m' }) + } + } + for (const match of text.matchAll(P2PKH_REGEX)) { + if (!seen.has(match[1])) { + seen.add(match[1]) + addresses.push({ address: match[1], type: 'p2pkh' }) + } + } + for (const match of text.matchAll(P2SH_REGEX)) { + if (!seen.has(match[1])) { + seen.add(match[1]) + addresses.push({ address: match[1], type: 'p2sh' }) + } + } + for (const match of text.matchAll(TESTNET_REGEX)) { + if (!seen.has(match[1].toLowerCase())) { + seen.add(match[1].toLowerCase()) + addresses.push({ address: match[1], type: 'testnet' }) + } + } + + return addresses +} + +export function detectBolt11Invoices(text: string): DetectedLightningInvoice[] { + const invoices: DetectedLightningInvoice[] = [] + const seen = new Set() + + for (const match of text.matchAll(BOLT11_REGEX)) { + const lower = match[1].toLowerCase() + if (!seen.has(lower)) { + seen.add(lower) + invoices.push({ invoice: match[1] }) + } + } + + return invoices +} + +export function detectBolt12Offers(text: string): DetectedBolt12Offer[] { + const offers: DetectedBolt12Offer[] = [] + const seen = new Set() + + for (const match of text.matchAll(BOLT12_REGEX)) { + const lower = match[1].toLowerCase() + if (!seen.has(lower)) { + seen.add(lower) + offers.push({ offer: match[1] }) + } + } + + return offers +} + +export function detectTxIds(text: string): DetectedTxId[] { + const txids: DetectedTxId[] = [] + const seen = new Set() + + for (const match of text.matchAll(TXID_REGEX)) { + if (!seen.has(match[1])) { + seen.add(match[1]) + txids.push({ txid: match[1] }) + } + } + + return txids +}