diff --git a/multi_llm_chatbot_backend/app/config.py b/multi_llm_chatbot_backend/app/config.py index 457e6c6..d421de1 100644 --- a/multi_llm_chatbot_backend/app/config.py +++ b/multi_llm_chatbot_backend/app/config.py @@ -21,7 +21,23 @@ # Pydantic models # --------------------------------------------------------------------------- -class FeatureConfig(BaseModel): +class _IconValidatorMixin(BaseModel): + """Validates that the ``icon`` field is a known Lucide icon name.""" + + @model_validator(mode="after") + def _validate_icon(self): + from app.utils.lucide_icons import get_valid_icon_names + + valid = get_valid_icon_names() + if valid and self.icon not in valid: + raise ValueError( + f"Unknown icon {self.icon!r}. " + f"Must be a valid Lucide icon name." + ) + return self + + +class FeatureConfig(_IconValidatorMixin): title: str = "" description: str = "" icon: str = "HelpCircle" @@ -53,7 +69,7 @@ class LoginConfig(BaseModel): academic_stages: List[AcademicStage] = [] -class ExampleCategory(BaseModel): +class ExampleCategory(_IconValidatorMixin): title: str icon: str = "BookOpen" color: str = "#3B82F6" @@ -66,7 +82,7 @@ class ChatPageConfig(BaseModel): examples: List[ExampleCategory] = [] -class PersonaItemConfig(BaseModel): +class PersonaItemConfig(_IconValidatorMixin): id: str name: str enabled: bool = True diff --git a/multi_llm_chatbot_backend/app/tests/test_lucide_icons.py b/multi_llm_chatbot_backend/app/tests/test_lucide_icons.py new file mode 100644 index 0000000..b9c2e79 --- /dev/null +++ b/multi_llm_chatbot_backend/app/tests/test_lucide_icons.py @@ -0,0 +1,74 @@ +import pytest +from pydantic import ValidationError +from app.utils.lucide_icons import get_valid_icon_names +from app.config import FeatureConfig, ExampleCategory, PersonaItemConfig + + +@pytest.fixture(autouse=True) +def _clear_icon_cache(): + get_valid_icon_names.cache_clear() + yield + get_valid_icon_names.cache_clear() + + +# --------------------------------------------------------------------------- +# Icon registry tests +# --------------------------------------------------------------------------- + + +def test_returns_known_icons(): + icons = get_valid_icon_names() + assert len(icons) > 1500 + assert "HelpCircle" in icons + assert "BookOpen" in icons + + +def test_rejects_invalid_icon(): + icons = get_valid_icon_names() + assert "NotARealIcon" not in icons + + +# --------------------------------------------------------------------------- +# Icon validation tests +# --------------------------------------------------------------------------- + + +def test_feature_config_accepts_valid_icon(): + feature = FeatureConfig(title="Test", description="desc", icon="BookOpen") + assert feature.icon == "BookOpen" + + +def test_example_category_accepts_valid_icon(): + example = ExampleCategory(title="Test", icon="Brain") + assert example.icon == "Brain" + + +def test_persona_item_accepts_valid_icon(): + persona = PersonaItemConfig(id="test", name="Test", icon="Heart") + assert persona.icon == "Heart" + + +def test_default_icons_are_valid(): + feature = FeatureConfig(title="Test", description="desc") + assert feature.icon == "HelpCircle" + + example = ExampleCategory(title="Test") + assert example.icon == "BookOpen" + + persona = PersonaItemConfig(id="test", name="Test") + assert persona.icon == "HelpCircle" + + +def test_feature_config_rejects_invalid_icon(): + with pytest.raises(ValidationError, match="Unknown icon"): + FeatureConfig(title="Test", description="desc", icon="NotARealIcon") + + +def test_example_category_rejects_invalid_icon(): + with pytest.raises(ValidationError, match="Unknown icon"): + ExampleCategory(title="Test", icon="TotallyFake") + + +def test_persona_item_rejects_invalid_icon(): + with pytest.raises(ValidationError, match="Unknown icon"): + PersonaItemConfig(id="test", name="Test", icon="Nope") diff --git a/multi_llm_chatbot_backend/app/utils/_lucide_icon_names.json b/multi_llm_chatbot_backend/app/utils/_lucide_icon_names.json new file mode 100644 index 0000000..8b43954 --- /dev/null +++ b/multi_llm_chatbot_backend/app/utils/_lucide_icon_names.json @@ -0,0 +1,1866 @@ +{ + "version": "0.544.0", + "icons": [ + "AArrowDown", + "AArrowUp", + "ALargeSmall", + "Accessibility", + "Activity", + "ActivitySquare", + "AirVent", + "Airplay", + "AlarmCheck", + "AlarmClock", + "AlarmClockCheck", + "AlarmClockMinus", + "AlarmClockOff", + "AlarmClockPlus", + "AlarmMinus", + "AlarmPlus", + "AlarmSmoke", + "Album", + "AlertCircle", + "AlertOctagon", + "AlertTriangle", + "AlignCenter", + "AlignCenterHorizontal", + "AlignCenterVertical", + "AlignEndHorizontal", + "AlignEndVertical", + "AlignHorizontalDistributeCenter", + "AlignHorizontalDistributeEnd", + "AlignHorizontalDistributeStart", + "AlignHorizontalJustifyCenter", + "AlignHorizontalJustifyEnd", + "AlignHorizontalJustifyStart", + "AlignHorizontalSpaceAround", + "AlignHorizontalSpaceBetween", + "AlignJustify", + "AlignLeft", + "AlignRight", + "AlignStartHorizontal", + "AlignStartVertical", + "AlignVerticalDistributeCenter", + "AlignVerticalDistributeEnd", + "AlignVerticalDistributeStart", + "AlignVerticalJustifyCenter", + "AlignVerticalJustifyEnd", + "AlignVerticalJustifyStart", + "AlignVerticalSpaceAround", + "AlignVerticalSpaceBetween", + "Ambulance", + "Ampersand", + "Ampersands", + "Amphora", + "Anchor", + "Angry", + "Annoyed", + "Antenna", + "Anvil", + "Aperture", + "AppWindow", + "AppWindowMac", + "Apple", + "Archive", + "ArchiveRestore", + "ArchiveX", + "AreaChart", + "Armchair", + "ArrowBigDown", + "ArrowBigDownDash", + "ArrowBigLeft", + "ArrowBigLeftDash", + "ArrowBigRight", + "ArrowBigRightDash", + "ArrowBigUp", + "ArrowBigUpDash", + "ArrowDown", + "ArrowDown01", + "ArrowDown10", + "ArrowDownAZ", + "ArrowDownAz", + "ArrowDownCircle", + "ArrowDownFromLine", + "ArrowDownLeft", + "ArrowDownLeftFromCircle", + "ArrowDownLeftFromSquare", + "ArrowDownLeftSquare", + "ArrowDownNarrowWide", + "ArrowDownRight", + "ArrowDownRightFromCircle", + "ArrowDownRightFromSquare", + "ArrowDownRightSquare", + "ArrowDownSquare", + "ArrowDownToDot", + "ArrowDownToLine", + "ArrowDownUp", + "ArrowDownWideNarrow", + "ArrowDownZA", + "ArrowDownZa", + "ArrowLeft", + "ArrowLeftCircle", + "ArrowLeftFromLine", + "ArrowLeftRight", + "ArrowLeftSquare", + "ArrowLeftToLine", + "ArrowRight", + "ArrowRightCircle", + "ArrowRightFromLine", + "ArrowRightLeft", + "ArrowRightSquare", + "ArrowRightToLine", + "ArrowUp", + "ArrowUp01", + "ArrowUp10", + "ArrowUpAZ", + "ArrowUpAz", + "ArrowUpCircle", + "ArrowUpDown", + "ArrowUpFromDot", + "ArrowUpFromLine", + "ArrowUpLeft", + "ArrowUpLeftFromCircle", + "ArrowUpLeftFromSquare", + "ArrowUpLeftSquare", + "ArrowUpNarrowWide", + "ArrowUpRight", + "ArrowUpRightFromCircle", + "ArrowUpRightFromSquare", + "ArrowUpRightSquare", + "ArrowUpSquare", + "ArrowUpToLine", + "ArrowUpWideNarrow", + "ArrowUpZA", + "ArrowUpZa", + "ArrowsUpFromLine", + "Asterisk", + "AsteriskSquare", + "AtSign", + "Atom", + "AudioLines", + "AudioWaveform", + "Award", + "Axe", + "Axis3D", + "Axis3d", + "Baby", + "Backpack", + "Badge", + "BadgeAlert", + "BadgeCent", + "BadgeCheck", + "BadgeDollarSign", + "BadgeEuro", + "BadgeHelp", + "BadgeIndianRupee", + "BadgeInfo", + "BadgeJapaneseYen", + "BadgeMinus", + "BadgePercent", + "BadgePlus", + "BadgePoundSterling", + "BadgeQuestionMark", + "BadgeRussianRuble", + "BadgeSwissFranc", + "BadgeTurkishLira", + "BadgeX", + "BaggageClaim", + "Ban", + "Banana", + "Bandage", + "Banknote", + "BanknoteArrowDown", + "BanknoteArrowUp", + "BanknoteX", + "BarChart", + "BarChart2", + "BarChart3", + "BarChart4", + "BarChartBig", + "BarChartHorizontal", + "BarChartHorizontalBig", + "Barcode", + "Barrel", + "Baseline", + "Bath", + "Battery", + "BatteryCharging", + "BatteryFull", + "BatteryLow", + "BatteryMedium", + "BatteryPlus", + "BatteryWarning", + "Beaker", + "Bean", + "BeanOff", + "Bed", + "BedDouble", + "BedSingle", + "Beef", + "Beer", + "BeerOff", + "Bell", + "BellDot", + "BellElectric", + "BellMinus", + "BellOff", + "BellPlus", + "BellRing", + "BetweenHorizonalEnd", + "BetweenHorizonalStart", + "BetweenHorizontalEnd", + "BetweenHorizontalStart", + "BetweenVerticalEnd", + "BetweenVerticalStart", + "BicepsFlexed", + "Bike", + "Binary", + "Binoculars", + "Biohazard", + "Bird", + "Bitcoin", + "Blend", + "Blinds", + "Blocks", + "Bluetooth", + "BluetoothConnected", + "BluetoothOff", + "BluetoothSearching", + "Bold", + "Bolt", + "Bomb", + "Bone", + "Book", + "BookA", + "BookAlert", + "BookAudio", + "BookCheck", + "BookCopy", + "BookDashed", + "BookDown", + "BookHeadphones", + "BookHeart", + "BookImage", + "BookKey", + "BookLock", + "BookMarked", + "BookMinus", + "BookOpen", + "BookOpenCheck", + "BookOpenText", + "BookPlus", + "BookTemplate", + "BookText", + "BookType", + "BookUp", + "BookUp2", + "BookUser", + "BookX", + "Bookmark", + "BookmarkCheck", + "BookmarkMinus", + "BookmarkPlus", + "BookmarkX", + "BoomBox", + "Bot", + "BotMessageSquare", + "BotOff", + "BottleWine", + "BowArrow", + "Box", + "BoxSelect", + "Boxes", + "Braces", + "Brackets", + "Brain", + "BrainCircuit", + "BrainCog", + "BrickWall", + "BrickWallFire", + "BrickWallShield", + "Briefcase", + "BriefcaseBusiness", + "BriefcaseConveyorBelt", + "BriefcaseMedical", + "BringToFront", + "Brush", + "BrushCleaning", + "Bubbles", + "Bug", + "BugOff", + "BugPlay", + "Building", + "Building2", + "Bus", + "BusFront", + "Cable", + "CableCar", + "Cake", + "CakeSlice", + "Calculator", + "Calendar", + "Calendar1", + "CalendarArrowDown", + "CalendarArrowUp", + "CalendarCheck", + "CalendarCheck2", + "CalendarClock", + "CalendarCog", + "CalendarDays", + "CalendarFold", + "CalendarHeart", + "CalendarMinus", + "CalendarMinus2", + "CalendarOff", + "CalendarPlus", + "CalendarPlus2", + "CalendarRange", + "CalendarSearch", + "CalendarSync", + "CalendarX", + "CalendarX2", + "Camera", + "CameraOff", + "CandlestickChart", + "Candy", + "CandyCane", + "CandyOff", + "Cannabis", + "Captions", + "CaptionsOff", + "Car", + "CarFront", + "CarTaxiFront", + "Caravan", + "CardSim", + "Carrot", + "CaseLower", + "CaseSensitive", + "CaseUpper", + "CassetteTape", + "Cast", + "Castle", + "Cat", + "Cctv", + "ChartArea", + "ChartBar", + "ChartBarBig", + "ChartBarDecreasing", + "ChartBarIncreasing", + "ChartBarStacked", + "ChartCandlestick", + "ChartColumn", + "ChartColumnBig", + "ChartColumnDecreasing", + "ChartColumnIncreasing", + "ChartColumnStacked", + "ChartGantt", + "ChartLine", + "ChartNetwork", + "ChartNoAxesColumn", + "ChartNoAxesColumnDecreasing", + "ChartNoAxesColumnIncreasing", + "ChartNoAxesCombined", + "ChartNoAxesGantt", + "ChartPie", + "ChartScatter", + "ChartSpline", + "Check", + "CheckCheck", + "CheckCircle", + "CheckCircle2", + "CheckLine", + "CheckSquare", + "CheckSquare2", + "ChefHat", + "Cherry", + "ChevronDown", + "ChevronDownCircle", + "ChevronDownSquare", + "ChevronFirst", + "ChevronLast", + "ChevronLeft", + "ChevronLeftCircle", + "ChevronLeftSquare", + "ChevronRight", + "ChevronRightCircle", + "ChevronRightSquare", + "ChevronUp", + "ChevronUpCircle", + "ChevronUpSquare", + "ChevronsDown", + "ChevronsDownUp", + "ChevronsLeft", + "ChevronsLeftRight", + "ChevronsLeftRightEllipsis", + "ChevronsRight", + "ChevronsRightLeft", + "ChevronsUp", + "ChevronsUpDown", + "Chrome", + "Chromium", + "Church", + "Cigarette", + "CigaretteOff", + "Circle", + "CircleAlert", + "CircleArrowDown", + "CircleArrowLeft", + "CircleArrowOutDownLeft", + "CircleArrowOutDownRight", + "CircleArrowOutUpLeft", + "CircleArrowOutUpRight", + "CircleArrowRight", + "CircleArrowUp", + "CircleCheck", + "CircleCheckBig", + "CircleChevronDown", + "CircleChevronLeft", + "CircleChevronRight", + "CircleChevronUp", + "CircleDashed", + "CircleDivide", + "CircleDollarSign", + "CircleDot", + "CircleDotDashed", + "CircleEllipsis", + "CircleEqual", + "CircleFadingArrowUp", + "CircleFadingPlus", + "CircleGauge", + "CircleHelp", + "CircleMinus", + "CircleOff", + "CircleParking", + "CircleParkingOff", + "CirclePause", + "CirclePercent", + "CirclePlay", + "CirclePlus", + "CirclePoundSterling", + "CirclePower", + "CircleQuestionMark", + "CircleSlash", + "CircleSlash2", + "CircleSlashed", + "CircleSmall", + "CircleStar", + "CircleStop", + "CircleUser", + "CircleUserRound", + "CircleX", + "CircuitBoard", + "Citrus", + "Clapperboard", + "Clipboard", + "ClipboardCheck", + "ClipboardClock", + "ClipboardCopy", + "ClipboardEdit", + "ClipboardList", + "ClipboardMinus", + "ClipboardPaste", + "ClipboardPen", + "ClipboardPenLine", + "ClipboardPlus", + "ClipboardSignature", + "ClipboardType", + "ClipboardX", + "Clock", + "Clock1", + "Clock10", + "Clock11", + "Clock12", + "Clock2", + "Clock3", + "Clock4", + "Clock5", + "Clock6", + "Clock7", + "Clock8", + "Clock9", + "ClockAlert", + "ClockArrowDown", + "ClockArrowUp", + "ClockFading", + "ClockPlus", + "ClosedCaption", + "Cloud", + "CloudAlert", + "CloudCheck", + "CloudCog", + "CloudDownload", + "CloudDrizzle", + "CloudFog", + "CloudHail", + "CloudLightning", + "CloudMoon", + "CloudMoonRain", + "CloudOff", + "CloudRain", + "CloudRainWind", + "CloudSnow", + "CloudSun", + "CloudSunRain", + "CloudUpload", + "Cloudy", + "Clover", + "Club", + "Code", + "Code2", + "CodeSquare", + "CodeXml", + "Codepen", + "Codesandbox", + "Coffee", + "Cog", + "Coins", + "Columns", + "Columns2", + "Columns3", + "Columns3Cog", + "Columns4", + "ColumnsSettings", + "Combine", + "Command", + "Compass", + "Component", + "Computer", + "ConciergeBell", + "Cone", + "Construction", + "Contact", + "Contact2", + "ContactRound", + "Container", + "Contrast", + "Cookie", + "CookingPot", + "Copy", + "CopyCheck", + "CopyMinus", + "CopyPlus", + "CopySlash", + "CopyX", + "Copyleft", + "Copyright", + "CornerDownLeft", + "CornerDownRight", + "CornerLeftDown", + "CornerLeftUp", + "CornerRightDown", + "CornerRightUp", + "CornerUpLeft", + "CornerUpRight", + "Cpu", + "CreativeCommons", + "CreditCard", + "Croissant", + "Crop", + "Cross", + "Crosshair", + "Crown", + "Cuboid", + "CupSoda", + "CurlyBraces", + "Currency", + "Cylinder", + "Dam", + "Database", + "DatabaseBackup", + "DatabaseZap", + "DecimalsArrowLeft", + "DecimalsArrowRight", + "Delete", + "Dessert", + "Diameter", + "Diamond", + "DiamondMinus", + "DiamondPercent", + "DiamondPlus", + "Dice1", + "Dice2", + "Dice3", + "Dice4", + "Dice5", + "Dice6", + "Dices", + "Diff", + "Disc", + "Disc2", + "Disc3", + "DiscAlbum", + "Divide", + "DivideCircle", + "DivideSquare", + "Dna", + "DnaOff", + "Dock", + "Dog", + "DollarSign", + "Donut", + "DoorClosed", + "DoorClosedLocked", + "DoorOpen", + "Dot", + "DotSquare", + "Download", + "DownloadCloud", + "DraftingCompass", + "Drama", + "Dribbble", + "Drill", + "Drone", + "Droplet", + "DropletOff", + "Droplets", + "Drum", + "Drumstick", + "Dumbbell", + "Ear", + "EarOff", + "Earth", + "EarthLock", + "Eclipse", + "Edit", + "Edit2", + "Edit3", + "Egg", + "EggFried", + "EggOff", + "Ellipsis", + "EllipsisVertical", + "Equal", + "EqualApproximately", + "EqualNot", + "EqualSquare", + "Eraser", + "EthernetPort", + "Euro", + "EvCharger", + "Expand", + "ExternalLink", + "Eye", + "EyeClosed", + "EyeOff", + "Facebook", + "Factory", + "Fan", + "FastForward", + "Feather", + "Fence", + "FerrisWheel", + "Figma", + "File", + "FileArchive", + "FileAudio", + "FileAudio2", + "FileAxis3D", + "FileAxis3d", + "FileBadge", + "FileBadge2", + "FileBarChart", + "FileBarChart2", + "FileBox", + "FileChartColumn", + "FileChartColumnIncreasing", + "FileChartLine", + "FileChartPie", + "FileCheck", + "FileCheck2", + "FileClock", + "FileCode", + "FileCode2", + "FileCog", + "FileCog2", + "FileDiff", + "FileDigit", + "FileDown", + "FileEdit", + "FileHeart", + "FileImage", + "FileInput", + "FileJson", + "FileJson2", + "FileKey", + "FileKey2", + "FileLineChart", + "FileLock", + "FileLock2", + "FileMinus", + "FileMinus2", + "FileMusic", + "FileOutput", + "FilePen", + "FilePenLine", + "FilePieChart", + "FilePlay", + "FilePlus", + "FilePlus2", + "FileQuestion", + "FileQuestionMark", + "FileScan", + "FileSearch", + "FileSearch2", + "FileSignature", + "FileSliders", + "FileSpreadsheet", + "FileStack", + "FileSymlink", + "FileTerminal", + "FileText", + "FileType", + "FileType2", + "FileUp", + "FileUser", + "FileVideo", + "FileVideo2", + "FileVideoCamera", + "FileVolume", + "FileVolume2", + "FileWarning", + "FileX", + "FileX2", + "Files", + "Film", + "Filter", + "FilterX", + "Fingerprint", + "FireExtinguisher", + "Fish", + "FishOff", + "FishSymbol", + "Flag", + "FlagOff", + "FlagTriangleLeft", + "FlagTriangleRight", + "Flame", + "FlameKindling", + "Flashlight", + "FlashlightOff", + "FlaskConical", + "FlaskConicalOff", + "FlaskRound", + "FlipHorizontal", + "FlipHorizontal2", + "FlipVertical", + "FlipVertical2", + "Flower", + "Flower2", + "Focus", + "FoldHorizontal", + "FoldVertical", + "Folder", + "FolderArchive", + "FolderCheck", + "FolderClock", + "FolderClosed", + "FolderCode", + "FolderCog", + "FolderCog2", + "FolderDot", + "FolderDown", + "FolderEdit", + "FolderGit", + "FolderGit2", + "FolderHeart", + "FolderInput", + "FolderKanban", + "FolderKey", + "FolderLock", + "FolderMinus", + "FolderOpen", + "FolderOpenDot", + "FolderOutput", + "FolderPen", + "FolderPlus", + "FolderRoot", + "FolderSearch", + "FolderSearch2", + "FolderSymlink", + "FolderSync", + "FolderTree", + "FolderUp", + "FolderX", + "Folders", + "Footprints", + "ForkKnife", + "ForkKnifeCrossed", + "Forklift", + "FormInput", + "Forward", + "Frame", + "Framer", + "Frown", + "Fuel", + "Fullscreen", + "FunctionSquare", + "Funnel", + "FunnelPlus", + "FunnelX", + "GalleryHorizontal", + "GalleryHorizontalEnd", + "GalleryThumbnails", + "GalleryVertical", + "GalleryVerticalEnd", + "Gamepad", + "Gamepad2", + "GanttChart", + "GanttChartSquare", + "Gauge", + "GaugeCircle", + "Gavel", + "Gem", + "GeorgianLari", + "Ghost", + "Gift", + "GitBranch", + "GitBranchPlus", + "GitCommit", + "GitCommitHorizontal", + "GitCommitVertical", + "GitCompare", + "GitCompareArrows", + "GitFork", + "GitGraph", + "GitMerge", + "GitPullRequest", + "GitPullRequestArrow", + "GitPullRequestClosed", + "GitPullRequestCreate", + "GitPullRequestCreateArrow", + "GitPullRequestDraft", + "Github", + "Gitlab", + "GlassWater", + "Glasses", + "Globe", + "Globe2", + "GlobeLock", + "Goal", + "Gpu", + "Grab", + "GraduationCap", + "Grape", + "Grid", + "Grid2X2", + "Grid2X2Check", + "Grid2X2Plus", + "Grid2X2X", + "Grid2x2", + "Grid2x2Check", + "Grid2x2Plus", + "Grid2x2X", + "Grid3X3", + "Grid3x2", + "Grid3x3", + "Grip", + "GripHorizontal", + "GripVertical", + "Group", + "Guitar", + "Ham", + "Hamburger", + "Hammer", + "Hand", + "HandCoins", + "HandFist", + "HandGrab", + "HandHeart", + "HandHelping", + "HandMetal", + "HandPlatter", + "Handbag", + "Handshake", + "HardDrive", + "HardDriveDownload", + "HardDriveUpload", + "HardHat", + "Hash", + "HatGlasses", + "Haze", + "HdmiPort", + "Heading", + "Heading1", + "Heading2", + "Heading3", + "Heading4", + "Heading5", + "Heading6", + "HeadphoneOff", + "Headphones", + "Headset", + "Heart", + "HeartCrack", + "HeartHandshake", + "HeartMinus", + "HeartOff", + "HeartPlus", + "HeartPulse", + "Heater", + "HelpCircle", + "HelpingHand", + "Hexagon", + "Highlighter", + "History", + "Home", + "Hop", + "HopOff", + "Hospital", + "Hotel", + "Hourglass", + "House", + "HouseHeart", + "HousePlug", + "HousePlus", + "HouseWifi", + "IceCream", + "IceCream2", + "IceCreamBowl", + "IceCreamCone", + "IdCard", + "IdCardLanyard", + "Image", + "ImageDown", + "ImageMinus", + "ImageOff", + "ImagePlay", + "ImagePlus", + "ImageUp", + "ImageUpscale", + "Images", + "Import", + "Inbox", + "Indent", + "IndentDecrease", + "IndentIncrease", + "IndianRupee", + "Infinity", + "Info", + "Inspect", + "InspectionPanel", + "Instagram", + "Italic", + "IterationCcw", + "IterationCw", + "JapaneseYen", + "Joystick", + "Kanban", + "KanbanSquare", + "KanbanSquareDashed", + "Kayak", + "Key", + "KeyRound", + "KeySquare", + "Keyboard", + "KeyboardMusic", + "KeyboardOff", + "Lamp", + "LampCeiling", + "LampDesk", + "LampFloor", + "LampWallDown", + "LampWallUp", + "LandPlot", + "Landmark", + "Languages", + "Laptop", + "Laptop2", + "LaptopMinimal", + "LaptopMinimalCheck", + "Lasso", + "LassoSelect", + "Laugh", + "Layers", + "Layers2", + "Layers3", + "Layout", + "LayoutDashboard", + "LayoutGrid", + "LayoutList", + "LayoutPanelLeft", + "LayoutPanelTop", + "LayoutTemplate", + "Leaf", + "LeafyGreen", + "Lectern", + "LetterText", + "Library", + "LibraryBig", + "LibrarySquare", + "LifeBuoy", + "Ligature", + "Lightbulb", + "LightbulbOff", + "LineChart", + "LineSquiggle", + "Link", + "Link2", + "Link2Off", + "Linkedin", + "List", + "ListCheck", + "ListChecks", + "ListChevronsDownUp", + "ListChevronsUpDown", + "ListCollapse", + "ListEnd", + "ListFilter", + "ListFilterPlus", + "ListIndentDecrease", + "ListIndentIncrease", + "ListMinus", + "ListMusic", + "ListOrdered", + "ListPlus", + "ListRestart", + "ListStart", + "ListTodo", + "ListTree", + "ListVideo", + "ListX", + "Loader", + "Loader2", + "LoaderCircle", + "LoaderPinwheel", + "Locate", + "LocateFixed", + "LocateOff", + "LocationEdit", + "Lock", + "LockKeyhole", + "LockKeyholeOpen", + "LockOpen", + "LogIn", + "LogOut", + "Logs", + "Lollipop", + "Luggage", + "MSquare", + "Magnet", + "Mail", + "MailCheck", + "MailMinus", + "MailOpen", + "MailPlus", + "MailQuestion", + "MailQuestionMark", + "MailSearch", + "MailWarning", + "MailX", + "Mailbox", + "Mails", + "Map", + "MapMinus", + "MapPin", + "MapPinCheck", + "MapPinCheckInside", + "MapPinHouse", + "MapPinMinus", + "MapPinMinusInside", + "MapPinOff", + "MapPinPen", + "MapPinPlus", + "MapPinPlusInside", + "MapPinX", + "MapPinXInside", + "MapPinned", + "MapPlus", + "Mars", + "MarsStroke", + "Martini", + "Maximize", + "Maximize2", + "Medal", + "Megaphone", + "MegaphoneOff", + "Meh", + "MemoryStick", + "Menu", + "MenuSquare", + "Merge", + "MessageCircle", + "MessageCircleCode", + "MessageCircleDashed", + "MessageCircleHeart", + "MessageCircleMore", + "MessageCircleOff", + "MessageCirclePlus", + "MessageCircleQuestion", + "MessageCircleQuestionMark", + "MessageCircleReply", + "MessageCircleWarning", + "MessageCircleX", + "MessageSquare", + "MessageSquareCode", + "MessageSquareDashed", + "MessageSquareDiff", + "MessageSquareDot", + "MessageSquareHeart", + "MessageSquareLock", + "MessageSquareMore", + "MessageSquareOff", + "MessageSquarePlus", + "MessageSquareQuote", + "MessageSquareReply", + "MessageSquareShare", + "MessageSquareText", + "MessageSquareWarning", + "MessageSquareX", + "MessagesSquare", + "Mic", + "Mic2", + "MicOff", + "MicVocal", + "Microchip", + "Microscope", + "Microwave", + "Milestone", + "Milk", + "MilkOff", + "Minimize", + "Minimize2", + "Minus", + "MinusCircle", + "MinusSquare", + "Monitor", + "MonitorCheck", + "MonitorCog", + "MonitorDot", + "MonitorDown", + "MonitorOff", + "MonitorPause", + "MonitorPlay", + "MonitorSmartphone", + "MonitorSpeaker", + "MonitorStop", + "MonitorUp", + "MonitorX", + "Moon", + "MoonStar", + "MoreHorizontal", + "MoreVertical", + "Mountain", + "MountainSnow", + "Mouse", + "MouseOff", + "MousePointer", + "MousePointer2", + "MousePointerBan", + "MousePointerClick", + "MousePointerSquareDashed", + "Move", + "Move3D", + "Move3d", + "MoveDiagonal", + "MoveDiagonal2", + "MoveDown", + "MoveDownLeft", + "MoveDownRight", + "MoveHorizontal", + "MoveLeft", + "MoveRight", + "MoveUp", + "MoveUpLeft", + "MoveUpRight", + "MoveVertical", + "Music", + "Music2", + "Music3", + "Music4", + "Navigation", + "Navigation2", + "Navigation2Off", + "NavigationOff", + "Network", + "Newspaper", + "Nfc", + "NonBinary", + "Notebook", + "NotebookPen", + "NotebookTabs", + "NotebookText", + "NotepadText", + "NotepadTextDashed", + "Nut", + "NutOff", + "Octagon", + "OctagonAlert", + "OctagonMinus", + "OctagonPause", + "OctagonX", + "Omega", + "Option", + "Orbit", + "Origami", + "Outdent", + "Package", + "Package2", + "PackageCheck", + "PackageMinus", + "PackageOpen", + "PackagePlus", + "PackageSearch", + "PackageX", + "PaintBucket", + "PaintRoller", + "Paintbrush", + "Paintbrush2", + "PaintbrushVertical", + "Palette", + "Palmtree", + "Panda", + "PanelBottom", + "PanelBottomClose", + "PanelBottomDashed", + "PanelBottomInactive", + "PanelBottomOpen", + "PanelLeft", + "PanelLeftClose", + "PanelLeftDashed", + "PanelLeftInactive", + "PanelLeftOpen", + "PanelLeftRightDashed", + "PanelRight", + "PanelRightClose", + "PanelRightDashed", + "PanelRightInactive", + "PanelRightOpen", + "PanelTop", + "PanelTopBottomDashed", + "PanelTopClose", + "PanelTopDashed", + "PanelTopInactive", + "PanelTopOpen", + "PanelsLeftBottom", + "PanelsLeftRight", + "PanelsRightBottom", + "PanelsTopBottom", + "PanelsTopLeft", + "Paperclip", + "Parentheses", + "ParkingCircle", + "ParkingCircleOff", + "ParkingMeter", + "ParkingSquare", + "ParkingSquareOff", + "PartyPopper", + "Pause", + "PauseCircle", + "PauseOctagon", + "PawPrint", + "PcCase", + "Pen", + "PenBox", + "PenLine", + "PenOff", + "PenSquare", + "PenTool", + "Pencil", + "PencilLine", + "PencilOff", + "PencilRuler", + "Pentagon", + "Percent", + "PercentCircle", + "PercentDiamond", + "PercentSquare", + "PersonStanding", + "PhilippinePeso", + "Phone", + "PhoneCall", + "PhoneForwarded", + "PhoneIncoming", + "PhoneMissed", + "PhoneOff", + "PhoneOutgoing", + "Pi", + "PiSquare", + "Piano", + "Pickaxe", + "PictureInPicture", + "PictureInPicture2", + "PieChart", + "PiggyBank", + "Pilcrow", + "PilcrowLeft", + "PilcrowRight", + "PilcrowSquare", + "Pill", + "PillBottle", + "Pin", + "PinOff", + "Pipette", + "Pizza", + "Plane", + "PlaneLanding", + "PlaneTakeoff", + "Play", + "PlayCircle", + "PlaySquare", + "Plug", + "Plug2", + "PlugZap", + "PlugZap2", + "Plus", + "PlusCircle", + "PlusSquare", + "Pocket", + "PocketKnife", + "Podcast", + "Pointer", + "PointerOff", + "Popcorn", + "Popsicle", + "PoundSterling", + "Power", + "PowerCircle", + "PowerOff", + "PowerSquare", + "Presentation", + "Printer", + "PrinterCheck", + "Projector", + "Proportions", + "Puzzle", + "Pyramid", + "QrCode", + "Quote", + "Rabbit", + "Radar", + "Radiation", + "Radical", + "Radio", + "RadioReceiver", + "RadioTower", + "Radius", + "RailSymbol", + "Rainbow", + "Rat", + "Ratio", + "Receipt", + "ReceiptCent", + "ReceiptEuro", + "ReceiptIndianRupee", + "ReceiptJapaneseYen", + "ReceiptPoundSterling", + "ReceiptRussianRuble", + "ReceiptSwissFranc", + "ReceiptText", + "ReceiptTurkishLira", + "RectangleCircle", + "RectangleEllipsis", + "RectangleGoggles", + "RectangleHorizontal", + "RectangleVertical", + "Recycle", + "Redo", + "Redo2", + "RedoDot", + "RefreshCcw", + "RefreshCcwDot", + "RefreshCw", + "RefreshCwOff", + "Refrigerator", + "Regex", + "RemoveFormatting", + "Repeat", + "Repeat1", + "Repeat2", + "Replace", + "ReplaceAll", + "Reply", + "ReplyAll", + "Rewind", + "Ribbon", + "Rocket", + "RockingChair", + "RollerCoaster", + "Rose", + "Rotate3D", + "Rotate3d", + "RotateCcw", + "RotateCcwKey", + "RotateCcwSquare", + "RotateCw", + "RotateCwSquare", + "Route", + "RouteOff", + "Router", + "Rows", + "Rows2", + "Rows3", + "Rows4", + "Rss", + "Ruler", + "RulerDimensionLine", + "RussianRuble", + "Sailboat", + "Salad", + "Sandwich", + "Satellite", + "SatelliteDish", + "SaudiRiyal", + "Save", + "SaveAll", + "SaveOff", + "Scale", + "Scale3D", + "Scale3d", + "Scaling", + "Scan", + "ScanBarcode", + "ScanEye", + "ScanFace", + "ScanHeart", + "ScanLine", + "ScanQrCode", + "ScanSearch", + "ScanText", + "ScatterChart", + "School", + "School2", + "Scissors", + "ScissorsLineDashed", + "ScissorsSquare", + "ScissorsSquareDashedBottom", + "ScreenShare", + "ScreenShareOff", + "Scroll", + "ScrollText", + "Search", + "SearchCheck", + "SearchCode", + "SearchSlash", + "SearchX", + "Section", + "Send", + "SendHorizonal", + "SendHorizontal", + "SendToBack", + "SeparatorHorizontal", + "SeparatorVertical", + "Server", + "ServerCog", + "ServerCrash", + "ServerOff", + "Settings", + "Settings2", + "Shapes", + "Share", + "Share2", + "Sheet", + "Shell", + "Shield", + "ShieldAlert", + "ShieldBan", + "ShieldCheck", + "ShieldClose", + "ShieldEllipsis", + "ShieldHalf", + "ShieldMinus", + "ShieldOff", + "ShieldPlus", + "ShieldQuestion", + "ShieldQuestionMark", + "ShieldUser", + "ShieldX", + "Ship", + "ShipWheel", + "Shirt", + "ShoppingBag", + "ShoppingBasket", + "ShoppingCart", + "Shovel", + "ShowerHead", + "Shredder", + "Shrimp", + "Shrink", + "Shrub", + "Shuffle", + "Sidebar", + "SidebarClose", + "SidebarOpen", + "Sigma", + "SigmaSquare", + "Signal", + "SignalHigh", + "SignalLow", + "SignalMedium", + "SignalZero", + "Signature", + "Signpost", + "SignpostBig", + "Siren", + "SkipBack", + "SkipForward", + "Skull", + "Slack", + "Slash", + "SlashSquare", + "Slice", + "Sliders", + "SlidersHorizontal", + "SlidersVertical", + "Smartphone", + "SmartphoneCharging", + "SmartphoneNfc", + "Smile", + "SmilePlus", + "Snail", + "Snowflake", + "SoapDispenserDroplet", + "Sofa", + "SortAsc", + "SortDesc", + "Soup", + "Space", + "Spade", + "Sparkle", + "Sparkles", + "Speaker", + "Speech", + "SpellCheck", + "SpellCheck2", + "Spline", + "SplinePointer", + "Split", + "SplitSquareHorizontal", + "SplitSquareVertical", + "Spool", + "Spotlight", + "SprayCan", + "Sprout", + "Square", + "SquareActivity", + "SquareArrowDown", + "SquareArrowDownLeft", + "SquareArrowDownRight", + "SquareArrowLeft", + "SquareArrowOutDownLeft", + "SquareArrowOutDownRight", + "SquareArrowOutUpLeft", + "SquareArrowOutUpRight", + "SquareArrowRight", + "SquareArrowUp", + "SquareArrowUpLeft", + "SquareArrowUpRight", + "SquareAsterisk", + "SquareBottomDashedScissors", + "SquareChartGantt", + "SquareCheck", + "SquareCheckBig", + "SquareChevronDown", + "SquareChevronLeft", + "SquareChevronRight", + "SquareChevronUp", + "SquareCode", + "SquareDashed", + "SquareDashedBottom", + "SquareDashedBottomCode", + "SquareDashedKanban", + "SquareDashedMousePointer", + "SquareDashedTopSolid", + "SquareDivide", + "SquareDot", + "SquareEqual", + "SquareFunction", + "SquareGanttChart", + "SquareKanban", + "SquareLibrary", + "SquareM", + "SquareMenu", + "SquareMinus", + "SquareMousePointer", + "SquareParking", + "SquareParkingOff", + "SquarePause", + "SquarePen", + "SquarePercent", + "SquarePi", + "SquarePilcrow", + "SquarePlay", + "SquarePlus", + "SquarePower", + "SquareRadical", + "SquareRoundCorner", + "SquareScissors", + "SquareSigma", + "SquareSlash", + "SquareSplitHorizontal", + "SquareSplitVertical", + "SquareSquare", + "SquareStack", + "SquareStar", + "SquareStop", + "SquareTerminal", + "SquareUser", + "SquareUserRound", + "SquareX", + "SquaresExclude", + "SquaresIntersect", + "SquaresSubtract", + "SquaresUnite", + "Squircle", + "SquircleDashed", + "Squirrel", + "Stamp", + "Star", + "StarHalf", + "StarOff", + "Stars", + "StepBack", + "StepForward", + "Stethoscope", + "Sticker", + "StickyNote", + "StopCircle", + "Store", + "StretchHorizontal", + "StretchVertical", + "Strikethrough", + "Subscript", + "Subtitles", + "Sun", + "SunDim", + "SunMedium", + "SunMoon", + "SunSnow", + "Sunrise", + "Sunset", + "Superscript", + "SwatchBook", + "SwissFranc", + "SwitchCamera", + "Sword", + "Swords", + "Syringe", + "Table", + "Table2", + "TableCellsMerge", + "TableCellsSplit", + "TableColumnsSplit", + "TableConfig", + "TableOfContents", + "TableProperties", + "TableRowsSplit", + "Tablet", + "TabletSmartphone", + "Tablets", + "Tag", + "Tags", + "Tally1", + "Tally2", + "Tally3", + "Tally4", + "Tally5", + "Tangent", + "Target", + "Telescope", + "Tent", + "TentTree", + "Terminal", + "TerminalSquare", + "TestTube", + "TestTube2", + "TestTubeDiagonal", + "TestTubes", + "Text", + "TextAlignCenter", + "TextAlignEnd", + "TextAlignJustify", + "TextAlignStart", + "TextCursor", + "TextCursorInput", + "TextInitial", + "TextQuote", + "TextSearch", + "TextSelect", + "TextSelection", + "TextWrap", + "Theater", + "Thermometer", + "ThermometerSnowflake", + "ThermometerSun", + "ThumbsDown", + "ThumbsUp", + "Ticket", + "TicketCheck", + "TicketMinus", + "TicketPercent", + "TicketPlus", + "TicketSlash", + "TicketX", + "Tickets", + "TicketsPlane", + "Timer", + "TimerOff", + "TimerReset", + "ToggleLeft", + "ToggleRight", + "Toilet", + "ToolCase", + "Tornado", + "Torus", + "Touchpad", + "TouchpadOff", + "TowerControl", + "ToyBrick", + "Tractor", + "TrafficCone", + "Train", + "TrainFront", + "TrainFrontTunnel", + "TrainTrack", + "TramFront", + "Transgender", + "Trash", + "Trash2", + "TreeDeciduous", + "TreePalm", + "TreePine", + "Trees", + "Trello", + "TrendingDown", + "TrendingUp", + "TrendingUpDown", + "Triangle", + "TriangleAlert", + "TriangleDashed", + "TriangleRight", + "Trophy", + "Truck", + "TruckElectric", + "TurkishLira", + "Turntable", + "Turtle", + "Tv", + "Tv2", + "TvMinimal", + "TvMinimalPlay", + "Twitch", + "Twitter", + "Type", + "TypeOutline", + "Umbrella", + "UmbrellaOff", + "Underline", + "Undo", + "Undo2", + "UndoDot", + "UnfoldHorizontal", + "UnfoldVertical", + "Ungroup", + "University", + "Unlink", + "Unlink2", + "Unlock", + "UnlockKeyhole", + "Unplug", + "Upload", + "UploadCloud", + "Usb", + "User", + "User2", + "UserCheck", + "UserCheck2", + "UserCircle", + "UserCircle2", + "UserCog", + "UserCog2", + "UserLock", + "UserMinus", + "UserMinus2", + "UserPen", + "UserPlus", + "UserPlus2", + "UserRound", + "UserRoundCheck", + "UserRoundCog", + "UserRoundMinus", + "UserRoundPen", + "UserRoundPlus", + "UserRoundSearch", + "UserRoundX", + "UserSearch", + "UserSquare", + "UserSquare2", + "UserStar", + "UserX", + "UserX2", + "Users", + "Users2", + "UsersRound", + "Utensils", + "UtensilsCrossed", + "UtilityPole", + "Variable", + "Vault", + "VectorSquare", + "Vegan", + "VenetianMask", + "Venus", + "VenusAndMars", + "Verified", + "Vibrate", + "VibrateOff", + "Video", + "VideoOff", + "Videotape", + "View", + "Voicemail", + "Volleyball", + "Volume", + "Volume1", + "Volume2", + "VolumeOff", + "VolumeX", + "Vote", + "Wallet", + "Wallet2", + "WalletCards", + "WalletMinimal", + "Wallpaper", + "Wand", + "Wand2", + "WandSparkles", + "Warehouse", + "WashingMachine", + "Watch", + "Waves", + "WavesLadder", + "Waypoints", + "Webcam", + "Webhook", + "WebhookOff", + "Weight", + "Wheat", + "WheatOff", + "WholeWord", + "Wifi", + "WifiCog", + "WifiHigh", + "WifiLow", + "WifiOff", + "WifiPen", + "WifiSync", + "WifiZero", + "Wind", + "WindArrowDown", + "Wine", + "WineOff", + "Workflow", + "Worm", + "WrapText", + "Wrench", + "X", + "XCircle", + "XOctagon", + "XSquare", + "Youtube", + "Zap", + "ZapOff", + "ZoomIn", + "ZoomOut" + ] +} diff --git a/multi_llm_chatbot_backend/app/utils/lucide_icons.py b/multi_llm_chatbot_backend/app/utils/lucide_icons.py new file mode 100644 index 0000000..11ed032 --- /dev/null +++ b/multi_llm_chatbot_backend/app/utils/lucide_icons.py @@ -0,0 +1,31 @@ +""" +Lucide icon name registry. + +Reads a pre-generated list of valid PascalCase Lucide icon names from a +bundled JSON file. Regenerate with:: + + python3 scripts/generate_icon_names.py +""" + +import json +import logging +from functools import lru_cache +from pathlib import Path + +logger = logging.getLogger(__name__) + +_ICON_NAMES_JSON = Path(__file__).resolve().parent / "_lucide_icon_names.json" + + +@lru_cache(maxsize=1) +def get_valid_icon_names() -> frozenset[str]: + """Return the set of valid PascalCase Lucide icon names.""" + try: + data = json.loads(_ICON_NAMES_JSON.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + logger.error("Failed to read icon names from %s: %s", _ICON_NAMES_JSON, exc) + return frozenset() + + icons = frozenset(data["icons"]) + logger.info("Loaded %d Lucide icon names (v%s)", len(icons), data.get("version")) + return icons diff --git a/scripts/generate_icon_names.py b/scripts/generate_icon_names.py new file mode 100644 index 0000000..1c39954 --- /dev/null +++ b/scripts/generate_icon_names.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Fetch valid Lucide icon names by introspecting the installed lucide-react package. + +Requires node_modules to be installed in phd-advisor-frontend/: + + cd phd-advisor-frontend && npm install + python3 scripts/generate_icon_names.py +""" + +import json +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +PACKAGE_JSON = REPO_ROOT / "phd-advisor-frontend" / "package.json" +FRONTEND_DIR = REPO_ROOT / "phd-advisor-frontend" +OUTPUT_FILE = ( + REPO_ROOT + / "multi_llm_chatbot_backend" + / "app" + / "utils" + / "_lucide_icon_names.json" +) + +JS_SNIPPET = """\ +const icons = require('lucide-react'); +const names = Object.keys(icons).filter(k => /^[A-Z]/.test(k) && !k.endsWith('Icon') && !k.startsWith('Lucide')).sort(); +console.log(JSON.stringify(names)); +""" + + +def main() -> int: + package_data = json.loads(PACKAGE_JSON.read_text(encoding="utf-8")) + version = package_data["dependencies"]["lucide-react"].lstrip("^~") + + node_modules = FRONTEND_DIR / "node_modules" / "lucide-react" + if not node_modules.exists(): + print( + f"Error: lucide-react not found at {node_modules}\n" + f"Run 'cd phd-advisor-frontend && npm install' first.", + file=sys.stderr, + ) + return 1 + + print(f"Introspecting lucide-react@{version} exports via Node ...") + result = subprocess.run( + ["node", "-e", JS_SNIPPET], + cwd=str(FRONTEND_DIR), + capture_output=True, + text=True, + ) + + if result.returncode != 0: + print(f"Node failed:\n{result.stderr}", file=sys.stderr) + return 1 + + pascal_names = json.loads(result.stdout) + + OUTPUT_FILE.write_text( + json.dumps({"version": version, "icons": pascal_names}, indent=2) + "\n", + encoding="utf-8", + ) + print(f"Wrote {len(pascal_names)} icon names (v{version}) to {OUTPUT_FILE}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())