This commit is contained in:
ajvpot
2025-07-03 00:00:00 +00:00
parent 7829b44868
commit 4db5d4c82e
3 changed files with 132 additions and 4 deletions
+14
View File
@@ -0,0 +1,14 @@
import { NextResponse } from "next/server";
import { getLatestChatMessages } from "@/lib/clickhouse/actions";
export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url);
const limit = parseInt(searchParams.get("limit") || "20", 10);
const before = searchParams.get("before") || undefined;
const messages = await getLatestChatMessages({ limit, before });
return NextResponse.json(messages);
} catch (error) {
return NextResponse.json({ error: "Failed to fetch chat messages" }, { status: 500 });
}
}
+94 -4
View File
@@ -1,10 +1,78 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import { MinusIcon, PlusIcon } from "@heroicons/react/24/outline";
interface ChatMessage {
ingest_timestamp: string;
origin: string;
mesh_timestamp: string;
packet: string;
path_len: number;
path: string;
channel_hash: string;
mac: string;
encrypted_message: string;
}
const PAGE_SIZE = 20;
function formatHex(hex: string): string {
// Add a space every 2 characters for readability
return hex.replace(/(.{2})/g, "$1 ").trim();
}
function formatLocalTime(utcString: string): string {
// Parse as UTC and display in local time
const utcDate = new Date(utcString + (utcString.endsWith('Z') ? '' : 'Z'));
return utcDate.toLocaleString();
}
export default function ChatBox() {
const [value, setValue] = useState("");
const [minimized, setMinimized] = useState(true);
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [lastBefore, setLastBefore] = useState<string | undefined>(undefined);
useEffect(() => {
if (!minimized) {
setMessages([]);
setHasMore(true);
setLastBefore(undefined);
fetchMessages(undefined, true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minimized]);
const fetchMessages = async (before?: string, replace = false) => {
setLoading(true);
try {
let url = `/api/chat?limit=${PAGE_SIZE}`;
if (before) url += `&before=${encodeURIComponent(before)}`;
const res = await fetch(url);
const data = await res.json();
if (Array.isArray(data)) {
setMessages((prev) => replace ? data : [...prev, ...data]);
setHasMore(data.length === PAGE_SIZE);
if (data.length > 0) {
setLastBefore(data[data.length - 1].ingest_timestamp);
}
} else {
setHasMore(false);
}
} catch {
setHasMore(false);
} finally {
setLoading(false);
}
};
const handleLoadMore = () => {
if (lastBefore) {
fetchMessages(lastBefore);
}
};
return (
<div
className={`w-80 bg-white dark:bg-neutral-900 rounded-lg shadow-lg flex flex-col ${
@@ -26,8 +94,30 @@ export default function ChatBox() {
</button>
</div>
{!minimized && (
<div className="flex-1 overflow-y-auto text-sm text-gray-700 dark:text-gray-200">
<div className="text-gray-400 text-center mt-8">Chat coming soon...</div>
<div className="flex-1 overflow-y-auto text-sm text-gray-700 dark:text-gray-200 flex flex-col-reverse">
<div className="flex flex-col-reverse gap-2">
{messages.length === 0 && !loading && (
<div className="text-gray-400 text-center mt-8">No chat messages found.</div>
)}
{messages.map((msg, i) => (
<div key={msg.ingest_timestamp + msg.origin + i} className="border-b border-gray-200 dark:border-neutral-800 pb-2 mb-2">
<div className="text-xs text-gray-400 flex items-center gap-2">
{formatLocalTime(msg.ingest_timestamp)} <span className="text-xs text-blue-600 dark:text-blue-400 font-mono">{msg.channel_hash}</span>
</div>
<div className="font-mono break-all whitespace-pre-wrap">{formatHex(msg.encrypted_message)}</div>
<div className="text-xs text-gray-300">from: {msg.origin}</div>
</div>
))}
{hasMore && (
<button
className="w-full py-2 bg-gray-100 dark:bg-neutral-800 rounded text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-neutral-700 mt-2"
onClick={handleLoadMore}
disabled={loading}
>
{loading ? "Loading..." : "Load more"}
</button>
)}
</div>
</div>
)}
</div>
+24
View File
@@ -43,4 +43,28 @@ export async function getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTyp
last_seen: string;
type: string;
}>;
}
export async function getLatestChatMessages({ limit = 20, before }: { limit?: number, before?: string } = {}) {
let where = [];
const params: Record<string, any> = { limit };
if (before) {
where.push('ingest_timestamp < {before:DateTime64}');
params.before = before;
}
const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
const query = `SELECT ingest_timestamp, origin, mesh_timestamp, packet, path_len, path, channel_hash, mac, hex(encrypted_message) AS encrypted_message FROM meshcore_public_channel_messages ${whereClause} ORDER BY ingest_timestamp DESC LIMIT {limit:UInt32}`;
const resultSet = await clickhouse.query({ query, query_params: params, format: 'JSONEachRow' });
const rows = await resultSet.json();
return rows as Array<{
ingest_timestamp: string;
origin: string;
mesh_timestamp: string;
packet: string;
path_len: number;
path: string;
channel_hash: string;
mac: string;
encrypted_message: string;
}>;
}