Unverified Commit 3d4f793f authored by memoclaw's avatar memoclaw Committed by GitHub

fix: include plain URLs and tags in memo snippet generation (#5688)

Co-authored-by: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 92d937b1
...@@ -244,15 +244,20 @@ func (s *service) GenerateSnippet(content []byte, maxLength int) (string, error) ...@@ -244,15 +244,20 @@ func (s *service) GenerateSnippet(content []byte, maxLength int) (string, error)
lastNodeWasBlock = false lastNodeWasBlock = false
// Only extract plain text nodes // Extract text from various node types
if textNode, ok := n.(*gast.Text); ok { switch node := n.(type) {
segment := textNode.Segment case *gast.Text:
segment := node.Segment
buf.Write(segment.Value(content)) buf.Write(segment.Value(content))
if node.SoftLineBreak() {
// Add space if this is a soft line break
if textNode.SoftLineBreak() {
buf.WriteByte(' ') buf.WriteByte(' ')
} }
case *gast.AutoLink:
buf.Write(node.URL(content))
return gast.WalkSkipChildren, nil
case *mast.TagNode:
buf.WriteByte('#')
buf.Write(node.Tag)
} }
// Stop walking if we've exceeded double the max length // Stop walking if we've exceeded double the max length
......
...@@ -94,6 +94,18 @@ func TestGenerateSnippet(t *testing.T) { ...@@ -94,6 +94,18 @@ func TestGenerateSnippet(t *testing.T) {
maxLength: 100, maxLength: 100,
expected: "Item 1 Item 2 Item 3", expected: "Item 1 Item 2 Item 3",
}, },
{
name: "plain URL autolink",
content: "https://usememos.com",
maxLength: 100,
expected: "https://usememos.com",
},
{
name: "text with plain URL",
content: "Check out https://usememos.com for more info.",
maxLength: 100,
expected: "Check out https://usememos.com for more info.",
},
} }
for _, tt := range tests { for _, tt := range tests {
...@@ -103,6 +115,35 @@ func TestGenerateSnippet(t *testing.T) { ...@@ -103,6 +115,35 @@ func TestGenerateSnippet(t *testing.T) {
assert.Equal(t, tt.expected, snippet) assert.Equal(t, tt.expected, snippet)
}) })
} }
// Test with tag extension enabled (matches production config).
svcWithTags := NewService(WithTagExtension())
tagTests := []struct {
name string
content string
maxLength int
expected string
}{
{
name: "tag only",
content: "#todo",
maxLength: 100,
expected: "#todo",
},
{
name: "text with tags",
content: "Remember to #review the #code",
maxLength: 100,
expected: "Remember to #review the #code",
},
}
for _, tt := range tagTests {
t.Run(tt.name, func(t *testing.T) {
snippet, err := svcWithTags.GenerateSnippet([]byte(tt.content), tt.maxLength)
require.NoError(t, err)
assert.Equal(t, tt.expected, snippet)
})
}
} }
func TestExtractProperties(t *testing.T) { func TestExtractProperties(t *testing.T) {
......
...@@ -141,7 +141,7 @@ const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [], ...@@ -141,7 +141,7 @@ const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [],
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden"> <div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
<div className="flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/30"> <div className="flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/30">
<PaperclipIcon className="w-3.5 h-3.5 text-muted-foreground" /> <PaperclipIcon className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-xs text-foreground">Attachments ({items.length})</span> <span className="text-xs text-muted-foreground">Attachments ({items.length})</span>
</div> </div>
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5"> <div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
......
...@@ -78,7 +78,7 @@ const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, par ...@@ -78,7 +78,7 @@ const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, par
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden"> <div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
<div className="flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/30"> <div className="flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/30">
<LinkIcon className="w-3.5 h-3.5 text-muted-foreground" /> <LinkIcon className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-xs text-foreground">Relations ({referenceRelations.length})</span> <span className="text-xs text-muted-foreground">Relations ({referenceRelations.length})</span>
</div> </div>
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5"> <div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
......
...@@ -3,6 +3,7 @@ import { Link } from "react-router-dom"; ...@@ -3,6 +3,7 @@ import { Link } from "react-router-dom";
import { extractMemoIdFromName } from "@/helpers/resource-names"; import { extractMemoIdFromName } from "@/helpers/resource-names";
import { useMemoComments } from "@/hooks/useMemoQueries"; import { useMemoComments } from "@/hooks/useMemoQueries";
import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext"; import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext";
import MemoSnippetLink from "./MemoSnippetLink";
const MemoCommentListView: React.FC = () => { const MemoCommentListView: React.FC = () => {
const { memo } = useMemoViewContext(); const { memo } = useMemoViewContext();
...@@ -23,7 +24,7 @@ const MemoCommentListView: React.FC = () => { ...@@ -23,7 +24,7 @@ const MemoCommentListView: React.FC = () => {
<span className="text-xs text-muted-foreground">Comments{commentAmount > 1 ? ` (${commentAmount})` : ""}</span> <span className="text-xs text-muted-foreground">Comments{commentAmount > 1 ? ` (${commentAmount})` : ""}</span>
<Link <Link
to={`/${memo.name}#comments`} to={`/${memo.name}#comments`}
className="flex items-center gap-0.5 text-xs text-muted-foreground hover:text-foreground hover:underline underline-offset-2 transition-colors" className="flex items-center gap-0.5 text-xs text-muted-foreground/80 hover:underline underline-offset-2 transition-colors"
> >
View all View all
<ArrowUpRightIcon className="w-3 h-3" /> <ArrowUpRightIcon className="w-3 h-3" />
...@@ -32,13 +33,13 @@ const MemoCommentListView: React.FC = () => { ...@@ -32,13 +33,13 @@ const MemoCommentListView: React.FC = () => {
{displayedComments.map((comment) => { {displayedComments.map((comment) => {
const uid = extractMemoIdFromName(comment.name); const uid = extractMemoIdFromName(comment.name);
return ( return (
<Link <MemoSnippetLink
key={comment.name} key={comment.name}
name={comment.name}
snippet={comment.snippet || comment.content}
to={`/${memo.name}#${uid}`} to={`/${memo.name}#${uid}`}
className="bg-muted/60 rounded-md px-2 py-1 text-xs text-muted-foreground truncate leading-relaxed hover:bg-muted transition-colors block" className="bg-muted/40 rounded-md"
> />
{comment.content}
</Link>
); );
})} })}
</div> </div>
......
import { Link } from "react-router-dom";
import { extractMemoIdFromName } from "@/helpers/resource-names";
import { cn } from "@/lib/utils";
interface MemoSnippetLinkProps {
name: string;
snippet: string;
to: string;
state?: object;
className?: string;
}
const MemoSnippetLink = ({ name, snippet, to, state, className }: MemoSnippetLinkProps) => {
const memoId = extractMemoIdFromName(name);
return (
<Link
className={cn(
"flex items-center gap-1 px-1 py-1 rounded text-xs text-muted-foreground hover:text-foreground hover:bg-accent/20 transition-colors group",
className,
)}
to={to}
viewTransition
state={state}
>
<span className="text-[8px] font-mono px-1 py-0.5 rounded border border-border bg-muted/40 group-hover:bg-accent/30 transition-colors shrink-0">
{memoId.slice(0, 6)}
</span>
<span className="truncate">{snippet}</span>
</Link>
);
};
export default MemoSnippetLink;
import { Link } from "react-router-dom";
import { extractMemoIdFromName } from "@/helpers/resource-names";
import { cn } from "@/lib/utils";
import type { MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb"; import type { MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb";
import MemoSnippetLink from "../MemoSnippetLink";
interface RelationCardProps { interface RelationCardProps {
memo: MemoRelation_Memo; memo: MemoRelation_Memo;
...@@ -10,23 +8,8 @@ interface RelationCardProps { ...@@ -10,23 +8,8 @@ interface RelationCardProps {
} }
const RelationCard = ({ memo, parentPage, className }: RelationCardProps) => { const RelationCard = ({ memo, parentPage, className }: RelationCardProps) => {
const memoId = extractMemoIdFromName(memo.name);
return ( return (
<Link <MemoSnippetLink name={memo.name} snippet={memo.snippet} to={`/${memo.name}`} state={{ from: parentPage }} className={className} />
className={cn(
"flex items-center gap-1 px-1 py-1 rounded text-xs text-muted-foreground hover:text-foreground hover:bg-accent/20 transition-colors group",
className,
)}
to={`/${memo.name}`}
viewTransition
state={{ from: parentPage }}
>
<span className="text-[8px] font-mono px-1 py-0.5 rounded border border-border bg-muted/40 group-hover:bg-accent/30 transition-colors shrink-0">
{memoId.slice(0, 6)}
</span>
<span className="truncate">{memo.snippet}</span>
</Link>
); );
}; };
......
...@@ -27,7 +27,7 @@ const SectionHeader = ({ icon: Icon, title, count, tabs }: SectionHeaderProps) = ...@@ -27,7 +27,7 @@ const SectionHeader = ({ icon: Icon, title, count, tabs }: SectionHeaderProps) =
onClick={tab.onClick} onClick={tab.onClick}
className={cn( className={cn(
"text-xs px-0 py-0 transition-colors", "text-xs px-0 py-0 transition-colors",
tab.active ? "text-foreground" : "text-muted-foreground hover:text-foreground", tab.active ? "text-muted-foreground" : "text-muted-foreground/60 hover:text-muted-foreground",
)} )}
> >
{tab.label} ({tab.count}) {tab.label} ({tab.count})
...@@ -37,7 +37,7 @@ const SectionHeader = ({ icon: Icon, title, count, tabs }: SectionHeaderProps) = ...@@ -37,7 +37,7 @@ const SectionHeader = ({ icon: Icon, title, count, tabs }: SectionHeaderProps) =
))} ))}
</div> </div>
) : ( ) : (
<span className="text-xs text-foreground"> <span className="text-xs text-muted-foreground">
{title} ({count}) {title} ({count})
</span> </span>
)} )}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment