普通视图

发现新文章,点击刷新页面。
昨天 — 2025年8月16日首页

《会聊天的文件筐:用 Next.js 打造“图音双绝”的上传组件》

作者 LeonGao
2025年8月15日 10:02

开场三句话

  1. 用户说:“发张图。”
  2. 用户说:“发段语音。”
  3. 你说:“稍等,我让浏览器先开个 AI 小灶。”

今天,我们要写一个聊天 UI 的上传组件,它既能识图又能辨音,还要保持界面优雅,像一位会魔法的管家。
(配图:一只端着托盘的小机器人,托盘上躺着一张猫咪照片和一只麦克风)


一、需求拆解:到底要上传什么?

类型 浏览器能做什么 我们要做什么
图片 <input type="file" accept="image/*"> 预览、压缩、OCR/打标签
音频 <input type="file" accept="audio/*"> or MediaRecorder 波形预览、转文字、情绪分析

一句话:浏览器负责“拿”,我们负责“看/听”


二、技术地图:从点击到 AI 的大脑

┌────────────┐     ┌──────────────┐     ┌──────────┐
│ 用户点击   │──→──│ 前端预览     │──→──│ 后端识别  │
│ input file │     │ canvas /    │     │ OCR /    │
└────────────┘     │ Web Audio   │     │ Whisper  │
                   └──────────────┘     └──────────┘

三、前端实现:React + TypeScript(Next.js 亦可)

3.1 组件骨架:一个 Hook 统治所有上传

// hooks/useUploader.ts
import { useState, useCallback } from 'react';

type FileType = 'image' | 'audio';

export function useUploader() {
  const [file, setFile] = useState<File | null>(null);
  const [preview, setPreview] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  const handleChange = useCallback(
    (type: FileType) => (e: React.ChangeEvent<HTMLInputElement>) => {
      const f = e.target.files?.[0];
      if (!f) return;
      setFile(f);
      setPreview(URL.createObjectURL(f));
      setLoading(true);
      // ⭐ 交给识别函数
      recognize(type, f).then((result) => {
        console.log('识别结果', result);
        setLoading(false);
      });
    },
    []
  );

  return { file, preview, loading, handleChange };
}

3.2 图片识别:浏览器端就能 OCR(tesseract.js)

// utils/recognize.ts
import Tesseract from 'tesseract.js';

export async function recognize(type: 'image' | 'audio', file: File) {
  if (type === 'image') {
    const { data: { text } } = await Tesseract.recognize(file, 'eng+chi_sim');
    return { text };
  }
  if (type === 'audio') {
    // 音频先上传,后端 Whisper 转文字,下文细讲
    const form = new FormData();
    form.append('audio', file);
    const res = await fetch('/api/transcribe', { method: 'POST', body: form });
    return res.json();
  }
}

浏览器里跑 OCR 就像让小学生在操场上背圆周率——能背,但跑不快。
所以我们只在小图离线场景用 tesseract.js,大图还是走后端 GPU。


3.3 音频录制:边录边传,体验拉满

// components/AudioRecorder.tsx
import { useState } from 'react';

export default function AudioRecorder({ onDone }: { onDone: (f: File) => void }) {
  const [recording, setRecording] = useState(false);
  const mediaRef = useRef<MediaRecorder | null>(null);

  const start = async () => {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    const mr = new MediaRecorder(stream, { mimeType: 'audio/webm' });
    const chunks: BlobPart[] = [];
    mr.ondataavailable = (e) => chunks.push(e.data);
    mr.onstop = () => {
      const blob = new Blob(chunks, { type: 'audio/webm' });
      onDone(new File([blob], 'speech.webm'));
    };
    mr.start();
    mediaRef.current = mr;
    setRecording(true);
  };

  const stop = () => {
    mediaRef.current?.stop();
    setRecording(false);
  };

  return (
    <>
      <button onClick={recording ? stop : start}>
        {recording ? '⏹️ 停止' : '🎤 录音'}
      </button>
    </>
  );
}

浏览器录音使用的是 MediaDevices.getUserMedia → MediaRecorder → Blob 这条“黄金管道”。
数据在内存里是 PCM 原始波形,压缩成 webm/opus 后才上传,节省 90% 流量。


四、后端识别:GPU 才是第一生产力

4.1 图片:OCR + 打标签(Python 示例,Next.js API Route 可调用)

# api/ocr.py  (FastAPI 伪代码)
from fastapi import UploadFile
import pytesseract, torch, timm

@app.post("/ocr")
async def ocr(file: UploadFile):
    img = await file.read()
    text = pytesseract.image_to_string(img, lang='eng+chi_sim')
    labels = model(img)  # timm 预训练 ResNet
    return {"text": text, "labels": labels}

4.2 音频:用 Whisper 转文字(OpenAI 开源版)

# api/transcribe.py
import whisper, tempfile, os

model = whisper.load_model("base")

@app.post("/transcribe")
async def transcribe(file: UploadFile):
    with tempfile.NamedTemporaryFile(delete=False, suffix=".webm") as tmp:
        tmp.write(await file.read())
        tmp.flush()
        result = model.transcribe(tmp.name, language='zh')
        os.unlink(tmp.name)
        return {"text": result["text"]}

Whisper 的「魔法」:把 30 秒音频切成 mel 频谱 → Transformer 编码 → 解码文字。
在 A100 上,转 30 秒音频只需 100 ms,比你泡咖啡还快。


五、前端 UI:让文件像聊天泡泡一样优雅

┌────────────────────────────┐
│  用户 A                   │
│  [猫咪照片预览]           │
│  🖼️ 识别:一只橘猫在打盹 │
└────────────────────────────┘

实现思路:

  1. 上传成功 → 本地先渲染占位泡泡(带 spinner)。
  2. 后端返回结果 → 更新泡泡内容(图片 + 文字 / 语音 + 文字)。
  3. 失败 → 泡泡变红色,重试按钮出现。

六、性能 & 体验小贴士

问题 解法
大图片 10 MB+ 浏览器 canvas.toBlob(file, 'image/jpeg', 0.8) 压缩
音频长 5 min+ 分片上传 + 后端流式转写
弱网 上传前存 IndexedDB,网络恢复后重试
隐私 敏感图片走本地 OCR,不上传

七、彩蛋:一行代码让上传支持拖拽

<div
  onDrop={(e) => {
    e.preventDefault();
    const f = e.dataTransfer.files[0];
    // 复用前面 useUploader 的逻辑
  }}
  onDragOver={(e) => e.preventDefault()}
  className="border-2 border-dashed border-gray-400 rounded p-8"
>
  📂 把文件扔进来
</div>

八、结语:上传的尽头,是理解

当 AI 把猫咪照片识别成“一只橘猫在打盹”,把语音转成“今晚吃什么?”时,
上传组件就不再是冷冰冰的 <input>,而是人类与算法握手言欢的桥梁

愿你写的每一个上传按钮,都能把比特变成诗。
祝你编码愉快,文件永不 413!

Next.js 嵌套路由与中间件:数据与逻辑的前哨站

作者 LeonGao
2025年8月16日 09:28

在现代 Web 应用的世界里,路由是城市道路,中间件是守在路口的警察,确保一切交通有序、安全。
Next.js 则是那位既懂交通规则、又能修路铺桥的工程师——你不仅可以在它的路网上自由嵌套路线,还可以让中间件在用户抵达目的地前对他们的身份、行李、甚至心情(如果你愿意)做检查。


一、嵌套路由的本质

在 Next.js 中,文件即路由的哲学让你少了很多配置文件的负担,但当你需要结构化复杂页面时,嵌套路由就派上了用场。

比如,你有一个博客系统:

/app
  /blog
    /page.js
    /[slug]
      /page.js
  • /blog → 博客列表页
  • /blog/[slug] → 某篇博客详情页

底层原理:

  • Next.js 会遍历 app 目录下的文件夹结构。
  • 目录名映射为 URL 路径,[param] 形式表示动态路由。
  • 嵌套文件夹会形成嵌套路由,父级路由可以包含 Layout,用来统一头部、底部、导航栏。

Layout 嵌套机制

// app/blog/layout.js
export default function BlogLayout({ children }) {
  return (
    <div>
      <header>Blog Header</header>
      <main>{children}</main>
    </div>
  );
}

这样 /blog/blog/[slug] 都会共享这个 BlogLayout,底层是组件树递归渲染,Next.js 会为每一层 Layout 建立独立 React 节点,从而实现父子关系。


二、中间件(Middleware)的使命

想象一下你有一个高档餐厅(网站),中间件就是门口的保安——

  • 检查身份证(鉴权)
  • 检查预订记录(权限控制)
  • 检查是否穿正装(条件跳转)
  • 甚至可以把迟到的人送去别的餐厅(重定向)

中间件的运行时机

  • 请求到达页面组件之前
  • 运行在 Edge Runtime(轻量、低延迟,全球分布)。
  • 可以读取和修改请求、响应。

底层机制

  • 你在项目根目录(或子目录)下放置一个 middleware.js 文件。
  • Next.js 会在构建时将它编译为 Edge Function。
  • 每次请求进入匹配的路径时,都会先经过中间件逻辑。

三、实战:嵌套路由 + 中间件

假设你有一个 /dashboard 路由和它的嵌套页面 /dashboard/settings,你想在用户进入这些页面前检查是否已登录。

目录结构:

/app
  /dashboard
    /page.js
    /settings
      /page.js
/middleware.js

中间件示例:

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(req) {
  const token = req.cookies.get('token');
  
  if (!token) {
    // 未登录则跳转到登录页
    return NextResponse.redirect(new URL('/login', req.url));
  }
  
  // 已登录则放行
  return NextResponse.next();
}

// 限制中间件只匹配 dashboard 路由
export const config = {
  matcher: ['/dashboard/:path*']
};

四、嵌套路由与中间件的协作

嵌套路由提供结构化的页面层级,而中间件提供请求入口的守卫
就像机场一样:

  • 嵌套路由 → 航站楼结构(国际、国内、贵宾厅等分区)
  • 中间件 → 安检口(拦截违禁品、核对身份、放行)

好处:

  1. 安全:中间件阻挡未授权用户。
  2. 体验:减少无意义的页面渲染。
  3. 性能:Edge Runtime 在边缘节点直接处理,不必每次回到主服务器。

五、最佳实践建议

  1. 中间件逻辑要精简

    • 它运行在边缘节点,不适合做大量计算。
    • 适合做快速判断、重定向、设置 cookie。
  2. 嵌套路由中 Layout 复用 UI

    • 避免重复代码,让不同子页面共享样式和结构。
  3. 分层控制

    • 根目录 middleware.js 管全局规则。
    • 子目录 middleware.js 处理局部规则(Next.js 13+ 支持子目录中间件)。

六、幽默的尾声

嵌套路由像一座大厦的楼层结构,
中间件是大门口的保安,
而 Next.js 是那位能帮你造大厦、请保安、装电梯的承包商。

有人会问:
“那如果我没中间件,直接让所有人进来会怎样?”
——那就像把你家 Wi-Fi 密码贴在电梯里,很快就会发现隔壁邻居比你还熟悉你的路由结构

AI UI 数据展示:Chart.js / Recharts + AI 总结文本的艺术

作者 LeonGao
2025年8月16日 09:25

在现代 Web 应用的世界里,数据展示早已不再是枯燥的表格,而是一场视觉盛宴。
就像数据是食材,AI 是大厨,Chart.js / Recharts 是精致的餐具——最终的 UI 是那道端上用户桌面的米其林级菜肴

本篇文章,我们将从底层原理到代码实践,一起探讨如何用 Chart.js / Recharts 绘制出优雅的数据图表,并用 AI 自动生成人类可读的总结文本


一、为什么 Chart.js 和 Recharts 是好搭档?

在前端图表界,Chart.js 和 Recharts 有点像两个性格不同的朋友:

  • Chart.js

    • 优势:轻量级,原生 Canvas 渲染,动画丝滑。
    • 适合场景:需要快速渲染高性能、交互不太复杂的图表。
    • 底层机制:直接操作 <canvas>,用 2D 渲染上下文绘制像素。
    • 缺点:配置复杂时需要更多手动调整。
  • Recharts

    • 优势:基于 React 组件化开发,易维护,语义化强。
    • 适合场景:React 项目里快速搭建交互性强的图表。
    • 底层机制:基于 D3.js 的计算和 SVG 渲染(矢量图,缩放不失真)。
    • 缺点:在大量数据点时性能可能逊色于 Canvas。

一句话总结

Chart.js 是“性能小钢炮”,Recharts 是“优雅绅士”,你可以根据业务场景选择或混用。


二、AI 在数据展示中的角色

如果 Chart.js 和 Recharts 是负责画画的,那 AI 就是旁白解说员

为什么需要 AI 文本总结?

  • 人眼对趋势敏感,但 AI 可以直接用自然语言告诉你结论
  • 当用户面对一堆数据曲线时,AI 可以说:“看!这个月的销售额比上月增长了 35%,并且主要得益于东南亚市场的爆发式增长。”

AI 的底层工作逻辑:

  1. 获取数据(JSON / API)。
  2. 特征提取:计算平均值、最大值、趋势变化率等。
  3. 语言生成:将这些特征喂给 AI 模型(如 GPT-4、Claude),让它用自然语言总结。
  4. 输出优化:控制字数、调整语气、加上商业或技术背景。

三、数据流的底层原理

一个典型的 AI UI 数据展示系统,数据流是这样的:

[ 数据源 API ][ 前端获取数据 fetch() ][ 数据处理:统计、归一化 ][ Chart.js / Recharts 渲染 ][ AI 调用接口生成总结文本 ][ 页面展示:图表 + 文本 ]

在底层实现里,Chart.js 会直接操作 Canvas 的像素点,而 Recharts 会在 DOM 中生成 <svg> 标签,并通过 D3.js 计算坐标和路径。

AI 部分则通常通过 HTTP 请求调用 LLM API,比如:

const summary = await fetch('/api/ai-summary', {
  method: 'POST',
  body: JSON.stringify({ data }),
});

在服务器上,你可能用 OpenAI API:

import OpenAI from 'openai';
const openai = new OpenAI();

const aiText = await openai.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [
    { role: "system", content: "你是数据分析师,帮我总结趋势" },
    { role: "user", content: JSON.stringify(data) }
  ]
});

四、实战示例:Chart.js + AI 总结

假设我们有一组销售额数据(按月份),我们先用 Chart.js 画出来,再调用 AI 给出文字总结。

import { Chart } from 'chart.js';

// 模拟数据
const salesData = [120, 140, 180, 160, 200, 250, 300];
const labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];

// 1. 绘制图表
new Chart(document.getElementById('salesChart'), {
  type: 'line',
  data: {
    labels,
    datasets: [{
      label: 'Monthly Sales',
      data: salesData,
      borderColor: '#4CAF50',
      fill: false
    }]
  }
});

// 2. 请求 AI 总结
async function getAISummary(data) {
  const res = await fetch('/api/ai-summary', {
    method: 'POST',
    body: JSON.stringify({ salesData: data })
  });
  const { summary } = await res.json();
  document.getElementById('summary').innerText = summary;
}

getAISummary(salesData);

五、Recharts + AI 总结(React 版本)

import { LineChart, Line, XAxis, YAxis, Tooltip } from 'recharts';

const data = [
  { month: 'Jan', sales: 120 },
  { month: 'Feb', sales: 140 },
  { month: 'Mar', sales: 180 },
  { month: 'Apr', sales: 160 },
  { month: 'May', sales: 200 },
  { month: 'Jun', sales: 250 },
  { month: 'Jul', sales: 300 }
];

export default function SalesChart() {
  return (
    <>
      <LineChart width={500} height={300} data={data}>
        <XAxis dataKey="month" />
        <YAxis />
        <Tooltip />
        <Line type="monotone" dataKey="sales" stroke="#4CAF50" />
      </LineChart>
      <div id="summary">AI 正在生成总结...</div>
    </>
  );
}

在 React 中,可以用 useEffect 触发 AI 总结的 API 调用,将数据传过去,再更新到 summary 状态中。


六、幽默的收尾

传统的数据展示是“看图说话”,
AI + 图表的组合是“看图不用说话,AI 替你说完”。

当 Chart.js 像年轻的涂鸦艺术家用画笔在 Canvas 上狂飙,
Recharts 则是那位戴着圆框眼镜、温文尔雅的 SVG 绘图师。
而 AI,就像后台的那位戏精,随时准备为你的数据配上旁白——
甚至会夸张地说你是下一个商业传奇。

昨天以前首页
❌
❌