Commit 1d289acc authored by  Joel  Oksanen's avatar Joel Oksanen
Browse files

Almost done with report

parent 11785997
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
*.pt *.pt
__pycache__/ __pycache__/
server/agent/amazon_data/ server/agent/amazon_data/
server/agent/SA/data/
server/agent/target_extraction/data/ server/agent/target_extraction/data/
server/agent/target_extraction/stanford-corenlp-full-2018-10-05 server/agent/target_extraction/stanford-corenlp-full-2018-10-05
server/agent/target_extraction/BERT/data/ server/agent/target_extraction/BERT/data/
......
This diff is collapsed.
import pandas as pd import pandas as pd
pd.set_option('display.max_colwidth', None) # pd.set_option('display.max_colwidth', None)
def get_reviews(category, meta_file, review_file): def get_reviews(category, meta_file, review_file):
metadata_iter = pd.read_json(meta_file, lines=True, chunksize=1000) metadata_iter = pd.read_json(meta_file, lines=True, chunksize=1000)
...@@ -37,4 +36,4 @@ def save_top_reviewed_products(n, category, meta_file, review_file, output_file, ...@@ -37,4 +36,4 @@ def save_top_reviewed_products(n, category, meta_file, review_file, output_file,
# 'amazon_data/Clothing_Shoes_and_Jewelry.json', 'amazon_data/reviews_for_watches.tsv', # 'amazon_data/Clothing_Shoes_and_Jewelry.json', 'amazon_data/reviews_for_watches.tsv',
# 'watch') # 'watch')
save_reviews('Necklaces', 'agent/amazon_data/meta_Clothing_Shoes_and_Jewelry.json', 'agent/amazon_data/Clothing_Shoes_and_Jewelry.json', 'agent/target_extraction/data/verified_necklace_reviews.tsv') save_reviews('Stand Mixers', 'amazon_data/meta_Home_and_Kitchen.json', 'amazon_data/Home_and_Kitchen.json', 'target_extraction/data/verified_stand_mixer_reviews.tsv')
...@@ -6,7 +6,7 @@ import time ...@@ -6,7 +6,7 @@ import time
class ConceptNet: class ConceptNet:
url = 'http://api.conceptnet.io' url = 'http://api.conceptnet.io'
limit = 5 limit = 50
def find_related(self, feature, rel): def find_related(self, feature, rel):
uri = '/query?node=/c/en/{feature}&other=/c/en&rel=/r/{rel}&limit={limit}'.format(feature=feature, rel=rel, limit=self.limit) uri = '/query?node=/c/en/{feature}&other=/c/en&rel=/r/{rel}&limit={limit}'.format(feature=feature, rel=rel, limit=self.limit)
...@@ -64,7 +64,7 @@ class ConceptNet: ...@@ -64,7 +64,7 @@ class ConceptNet:
synonyms.add(node.name) synonyms.add(node.name)
return synonyms return synonyms
def sub_features_for_node(self, node): def sub_features_for_argument(self, argument):
rels = ['UsedFor', 'HasA', 'CapableOf', 'Causes', 'HasSubevent', 'HasProperty', 'MadeOf'] rels = ['UsedFor', 'HasA', 'CapableOf', 'Causes', 'HasSubevent', 'HasProperty', 'MadeOf']
features = set() features = set()
...@@ -72,10 +72,13 @@ class ConceptNet: ...@@ -72,10 +72,13 @@ class ConceptNet:
threads = [] threads = []
for rel in rels: for rel in rels:
t = threading.Thread(target=self.append_result, args=(node.name, rel, features, lock)) t = threading.Thread(target=self.append_result, args=(argument, rel, features, lock))
t.start() t.start()
threads.append(t) threads.append(t)
for t in threads: for t in threads:
t.join() t.join()
return features return features
cnet = ConceptNet()
print(cnet.find_related('sweater', 'MadeOf'))
...@@ -30,6 +30,8 @@ ...@@ -30,6 +30,8 @@
9449FE4F240533FD00025F70 /* QueryOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9449FE4E240533FD00025F70 /* QueryOptionView.swift */; }; 9449FE4F240533FD00025F70 /* QueryOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9449FE4E240533FD00025F70 /* QueryOptionView.swift */; };
9449FE5124053DA500025F70 /* ChatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9449FE5024053DA500025F70 /* ChatManager.swift */; }; 9449FE5124053DA500025F70 /* ChatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9449FE5024053DA500025F70 /* ChatManager.swift */; };
945E6BBD2493931300C0DCAC /* ArgumentText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945E6BBC2493931300C0DCAC /* ArgumentText.swift */; }; 945E6BBD2493931300C0DCAC /* ArgumentText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945E6BBC2493931300C0DCAC /* ArgumentText.swift */; };
945E6BC12494084C00C0DCAC /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945E6BC02494084C00C0DCAC /* SearchView.swift */; };
945E6BC32494097400C0DCAC /* ADAView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945E6BC22494097400C0DCAC /* ADAView.swift */; };
94BE1EEB2407E26900741749 /* RatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BE1EEA2407E26900741749 /* RatingView.swift */; }; 94BE1EEB2407E26900741749 /* RatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BE1EEA2407E26900741749 /* RatingView.swift */; };
94BE1EED240800D800741749 /* InitResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BE1EEC240800D800741749 /* InitResponse.swift */; }; 94BE1EED240800D800741749 /* InitResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BE1EEC240800D800741749 /* InitResponse.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
...@@ -60,6 +62,8 @@ ...@@ -60,6 +62,8 @@
9449FE4E240533FD00025F70 /* QueryOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryOptionView.swift; sourceTree = "<group>"; }; 9449FE4E240533FD00025F70 /* QueryOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryOptionView.swift; sourceTree = "<group>"; };
9449FE5024053DA500025F70 /* ChatManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatManager.swift; sourceTree = "<group>"; }; 9449FE5024053DA500025F70 /* ChatManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatManager.swift; sourceTree = "<group>"; };
945E6BBC2493931300C0DCAC /* ArgumentText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArgumentText.swift; sourceTree = "<group>"; }; 945E6BBC2493931300C0DCAC /* ArgumentText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArgumentText.swift; sourceTree = "<group>"; };
945E6BC02494084C00C0DCAC /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
945E6BC22494097400C0DCAC /* ADAView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ADAView.swift; sourceTree = "<group>"; };
94BE1EEA2407E26900741749 /* RatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingView.swift; sourceTree = "<group>"; }; 94BE1EEA2407E26900741749 /* RatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingView.swift; sourceTree = "<group>"; };
94BE1EEC240800D800741749 /* InitResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitResponse.swift; sourceTree = "<group>"; }; 94BE1EEC240800D800741749 /* InitResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitResponse.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
...@@ -97,6 +101,8 @@ ...@@ -97,6 +101,8 @@
9449FE1A2402C84F00025F70 /* AppDelegate.swift */, 9449FE1A2402C84F00025F70 /* AppDelegate.swift */,
9449FE1C2402C84F00025F70 /* SceneDelegate.swift */, 9449FE1C2402C84F00025F70 /* SceneDelegate.swift */,
9449FE1E2402C84F00025F70 /* ContentView.swift */, 9449FE1E2402C84F00025F70 /* ContentView.swift */,
945E6BC02494084C00C0DCAC /* SearchView.swift */,
945E6BC22494097400C0DCAC /* ADAView.swift */,
9449FE342402CCDA00025F70 /* ConnectionManager.swift */, 9449FE342402CCDA00025F70 /* ConnectionManager.swift */,
94BE1EEC240800D800741749 /* InitResponse.swift */, 94BE1EEC240800D800741749 /* InitResponse.swift */,
9449FE3F2403EDD300025F70 /* Product */, 9449FE3F2403EDD300025F70 /* Product */,
...@@ -233,8 +239,10 @@ ...@@ -233,8 +239,10 @@
9449FE452403F0A600025F70 /* Sender.swift in Sources */, 9449FE452403F0A600025F70 /* Sender.swift in Sources */,
9449FE4D2404561400025F70 /* ArgumentQuery.swift in Sources */, 9449FE4D2404561400025F70 /* ArgumentQuery.swift in Sources */,
9449FE4924042D6500025F70 /* FeatureView.swift in Sources */, 9449FE4924042D6500025F70 /* FeatureView.swift in Sources */,
945E6BC12494084C00C0DCAC /* SearchView.swift in Sources */,
9449FE312402C90800025F70 /* ChatView.swift in Sources */, 9449FE312402C90800025F70 /* ChatView.swift in Sources */,
9449FE5124053DA500025F70 /* ChatManager.swift in Sources */, 9449FE5124053DA500025F70 /* ChatManager.swift in Sources */,
945E6BC32494097400C0DCAC /* ADAView.swift in Sources */,
945E6BBD2493931300C0DCAC /* ArgumentText.swift in Sources */, 945E6BBD2493931300C0DCAC /* ArgumentText.swift in Sources */,
9449FE352402CCDA00025F70 /* ConnectionManager.swift in Sources */, 9449FE352402CCDA00025F70 /* ConnectionManager.swift in Sources */,
9449FE1D2402C84F00025F70 /* SceneDelegate.swift in Sources */, 9449FE1D2402C84F00025F70 /* SceneDelegate.swift in Sources */,
......
...@@ -8,13 +8,19 @@ ...@@ -8,13 +8,19 @@
import SwiftUI import SwiftUI
struct ProductView: View { struct ADAView: View {
@ObservedObject var connectionManager: ConnectionManager @ObservedObject var connectionManager: ConnectionManager
var drag: some Gesture {
DragGesture()
.onEnded { _ in self.connectionManager.quitMessaging() }
}
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
ProductView(connectionManager: connectionManager) ProductView(connectionManager: connectionManager)
.zIndex(10) .zIndex(10)
.gesture(drag)
ChatView(connectionManager: connectionManager) ChatView(connectionManager: connectionManager)
.zIndex(0) .zIndex(0)
} }
......
...@@ -30,12 +30,16 @@ struct ADAMessage: Message, Decodable, Equatable { ...@@ -30,12 +30,16 @@ struct ADAMessage: Message, Decodable, Equatable {
textWithArguments += text.arguments[i] textWithArguments += text.arguments[i]
} }
} }
let attributes = [NSAttributedString.Key.font: UIFont(name: "Helvetica Neue", size: 14)!, let attributes = [NSAttributedString.Key.font: UIFont(name: text.style == .QUOT ? "HelveticaNeue-Italic" : "Helvetica Neue", size: 14)!,
NSAttributedString.Key.foregroundColor: UIColor.white] NSAttributedString.Key.foregroundColor: UIColor.white]
let attributedString = NSMutableAttributedString(string: textWithArguments, attributes: attributes) let attributedString = NSMutableAttributedString(string: textWithArguments, attributes: attributes)
print(attributedString)
for (start, len) in argumentLocations { for (start, len) in argumentLocations {
attributedString.addAttribute(NSAttributedString.Key.font, value: text.style.getFont(), range: NSMakeRange(start, len)) attributedString.addAttribute(NSAttributedString.Key.font,
value: UIFont(name: text.style == .QUOT ? "HelveticaNeue-BoldItalic" : "HelveticaNeue-Bold", size: 14)!,
range: NSMakeRange(start, len))
// attributedString.addAttribute(NSAttributedString.Key.foregroundColor,
// value: UIColor(red: 140/255, green: 100/255, blue: 15/255, alpha:1.0).cgColor,
// range: NSMakeRange(start, len))
} }
return attributedString return attributedString
} }
......
...@@ -21,11 +21,4 @@ enum TextStyle: String, Decodable { ...@@ -21,11 +21,4 @@ enum TextStyle: String, Decodable {
case ARG case ARG
case QUOT case QUOT
func getFont() -> UIFont {
switch self {
case .ARG: return UIFont(name: "HelveticaNeue-Bold", size: 14)!
case .QUOT: return UIFont(name: "HelveticaNeue-Italic", size: 14)!
}
}
} }
...@@ -15,7 +15,7 @@ struct MessageView: View { ...@@ -15,7 +15,7 @@ struct MessageView: View {
Sender.ADA: Color(red: 242/255, green: 159/255, blue: 31/255), Sender.ADA: Color(red: 242/255, green: 159/255, blue: 31/255),
Sender.USER: Color(red: 35/255, green: 45/255, blue: 62/255) Sender.USER: Color(red: 35/255, green: 45/255, blue: 62/255)
] ]
let maxWidth: CGFloat = 300 let maxWidth: CGFloat = 260
let message: Message let message: Message
let query: ArgumentQuery? let query: ArgumentQuery?
...@@ -45,11 +45,9 @@ struct MessageView: View { ...@@ -45,11 +45,9 @@ struct MessageView: View {
Label(maxWidth: maxWidth, attributedText: self.message.getAttributedText()) Label(maxWidth: maxWidth, attributedText: self.message.getAttributedText())
.foregroundColor(Color.white) .foregroundColor(Color.white)
// .font(Font.custom("Helvetica Neue", size: 14))
.fixedSize(horizontal: true, vertical: true) .fixedSize(horizontal: true, vertical: true)
.padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20)) .padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20))
.background(MessageBubble(sender: message.sender).foregroundColor(sent ? bubbleColors[message.sender] : FeatureView.bubbleColor)) .background(MessageBubble(sender: message.sender).foregroundColor(sent ? bubbleColors[message.sender] : FeatureView.bubbleColor))
// .frame(minWidth: 0, maxWidth: maxWidth, alignment: message.sender == .ADA ? .leading : .trailing)
.onTapGesture { .onTapGesture {
withAnimation(.easeInOut(duration: 0.3)) { withAnimation(.easeInOut(duration: 0.3)) {
if self.message.sender == .ADA { if self.message.sender == .ADA {
...@@ -77,14 +75,6 @@ struct MessageView: View { ...@@ -77,14 +75,6 @@ struct MessageView: View {
} }
struct Label: UIViewRepresentable { struct Label: UIViewRepresentable {
// typealias TheUIView = UILabel
// fileprivate var configuration = { (view: TheUIView) in }
//
// func makeUIView(context: UIViewRepresentableContext<Self>) -> TheUIView { TheUIView() }
// func updateUIView(_ uiView: TheUIView, context: UIViewRepresentableContext<Self>) {
// configuration(uiView)
// }
var maxWidth: CGFloat var maxWidth: CGFloat
var attributedText: NSAttributedString var attributedText: NSAttributedString
......
...@@ -9,15 +9,18 @@ ...@@ -9,15 +9,18 @@
import SwiftUI import SwiftUI
class ConnectionManager: ObservableObject { class ConnectionManager: ObservableObject {
@Published var product = Product() @Published var products = [Product]()
@Published var product: Product!
@Published var messaging: Bool = false
@Published var messages: [Message] = [Message]() @Published var messages: [Message] = [Message]()
private let ip = "192.168.1.104" private let ip = "192.168.1.104"
private let port = "8000" private let port = "8000"
private var messageQueue: [Message]? = nil private var messageQueue: [Message]? = nil
private var productMap = [String: Product]()
init() { init() {
requestProduct(id: "B00005UP2N") requestProducts()
// B00RTGK0N0 - red Canon camera // B00RTGK0N0 - red Canon camera
// B004J3V90Y - Canon T3i // B004J3V90Y - Canon T3i
// B0012YA85A - Canon Rebel XSI // B0012YA85A - Canon Rebel XSI
...@@ -25,22 +28,21 @@ class ConnectionManager: ObservableObject { ...@@ -25,22 +28,21 @@ class ConnectionManager: ObservableObject {
// B0075SUK14 - Backpack // B0075SUK14 - Backpack
// B000AYW0M2 - Watch // B000AYW0M2 - Watch
// B000ZKA0J6 - Starcraft game // B000ZKA0J6 - Starcraft game
// B00005UP2N - Mixer // B00005UP2N - Silver Mixer
// B0001HLTTI - Another Mixer
} }
private func requestProduct(id: String) { private func requestProducts() {
let url = URL(string: "http://" + ip + ":" + port + "/ios_server/product/?id=" + id)! let url = URL(string: "http://" + ip + ":" + port + "/ios_server/products")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return } guard let data = data else { return }
do { do {
print(data) let resp = try JSONDecoder().decode([ProductInfo].self, from: data)
let resp = try JSONDecoder().decode(InitResponse.self, from: data)
DispatchQueue.main.async { DispatchQueue.main.async {
self.requestImage(at: resp.productInfo.imageURL) for productInfo in resp {
self.product.name = resp.productInfo.name self.requestImage(for: productInfo)
self.product.starRating = resp.productInfo.starRating }
self.addMessage(resp.message)
} }
} catch let parseError { } catch let parseError {
print(parseError) print(parseError)
...@@ -52,20 +54,44 @@ class ConnectionManager: ObservableObject { ...@@ -52,20 +54,44 @@ class ConnectionManager: ObservableObject {
task.resume() task.resume()
} }
private func requestImage(at urlString: String) { private func requestImage(for productInfo: ProductInfo) {
let url = URL(string: urlString)! let url = URL(string: productInfo.imageURL)!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return } guard let data = data else { return }
if let image = UIImage(data: data) { if let image = UIImage(data: data) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.product.image = image let product = Product(id: productInfo.id, name: productInfo.name, starRating: productInfo.starRating, image: image)
self.productMap[productInfo.id] = product
self.products.append(product)
} }
} }
} }
task.resume() task.resume()
} }
func requestProduct(id: String) {
let url = URL(string: "http://" + ip + ":" + port + "/ios_server/product?id=" + id)!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
do {
let resp = try JSONDecoder().decode(ADAMessage.self, from: data)
DispatchQueue.main.async {
self.addMessage(resp)
self.product = self.productMap[id]
self.messaging = true
}
} catch let parseError {
print(parseError)
DispatchQueue.main.async {
// Handle error in UI
}
}
}
task.resume()
}
func sendQuery(_ query: ArgumentQuery) { func sendQuery(_ query: ArgumentQuery) {
let url = URL(string: "http://" + ip + ":" + port + "/ios_server/message/")! let url = URL(string: "http://" + ip + ":" + port + "/ios_server/message/")!
var request = URLRequest(url: url) var request = URLRequest(url: url)
...@@ -127,6 +153,14 @@ class ConnectionManager: ObservableObject { ...@@ -127,6 +153,14 @@ class ConnectionManager: ObservableObject {
} }
} }
func quitMessaging() {
self.messaging = false
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.messages = [Message]()
self.product = nil
}
}
private func handleClientError(error: Error) { private func handleClientError(error: Error) {
print("Client error occurred") print("Client error occurred")
} }
......
...@@ -10,15 +10,16 @@ import SwiftUI ...@@ -10,15 +10,16 @@ import SwiftUI
struct ContentView: View { struct ContentView: View {
@ObservedObject var connectionManager = ConnectionManager() @ObservedObject var connectionManager = ConnectionManager()
@State private var searching: Bool = true
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
ProductView(connectionManager: connectionManager) if !connectionManager.messaging {
.zIndex(10) SearchView(connectionManager: connectionManager)
ChatView(connectionManager: connectionManager) } else {
.zIndex(0) ADAView(connectionManager: connectionManager)
}
} }
.edgesIgnoringSafeArea(.all)
.background(Color.black) .background(Color.black)
} }
} }
......
...@@ -8,16 +8,11 @@ ...@@ -8,16 +8,11 @@
import UIKit import UIKit
struct Product { struct Product: Identifiable {
var id: String
var name: String var name: String
var starRating: Double var starRating: Double
var image: UIImage var image: UIImage
init() {
self.name = ""
self.starRating = 0
self.image = UIImage()
}
} }
...@@ -26,7 +26,7 @@ struct ProductView: View { ...@@ -26,7 +26,7 @@ struct ProductView: View {
Text(connectionManager.product.name) Text(connectionManager.product.name)
.font(Font.custom("Helvetica Neue", size: 14)) .font(Font.custom("Helvetica Neue", size: 14))
.foregroundColor(Color.gray) .foregroundColor(Color.gray)
RatingView(starRating: $connectionManager.product.starRating) RatingView(starRating: connectionManager.product.starRating)
} }
Spacer() Spacer()
Image(uiImage: connectionManager.product.image) Image(uiImage: connectionManager.product.image)
......
...@@ -10,7 +10,7 @@ import SwiftUI ...@@ -10,7 +10,7 @@ import SwiftUI
struct RatingView: View { struct RatingView: View {
let dim: CGFloat = 16 let dim: CGFloat = 16
@Binding var starRating: Double var starRating: Double
var fullStars: Int { var fullStars: Int {
get { get {
return Int(starRating.rounded(.down)) return Int(starRating.rounded(.down))
......
...@@ -6,4 +6,61 @@ ...@@ -6,4 +6,61 @@
// Copyright © 2020 Joel Oksanen. All rights reserved. // Copyright © 2020 Joel Oksanen. All rights reserved.
// //
import Foundation import SwiftUI
struct SearchView: View {
@ObservedObject var connectionManager: ConnectionManager
let height: CGFloat = 120
var body: some View {
VStack(spacing: 0) {
ZStack {
Rectangle()
.foregroundColor(Color.white)
.frame(height: height)
.shadow(color: Color(.sRGB, white: 0, opacity: 0.1), radius: 10, x: 0, y: 0)
VStack {
Spacer()
.frame(height: 30)
HStack {
Text("Products")
.font(Font.custom("HelveticaNeue-Bold", size: 28))
.foregroundColor(Color.gray)
.padding(EdgeInsets(top: 0, leading: 30, bottom: 0, trailing: 0))
Spacer()
}
}
}
.zIndex(10)
List(connectionManager.products) { product in
ProductRow(product: product)
.onTapGesture {
self.connectionManager.requestProduct(id: product.id)
}
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct ProductRow: View {
var product: Product
var body: some View {
HStack(spacing: 0) {
Image(uiImage: product.image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
.border(Color(white: 0.85), width: 1)
.padding(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 20))
VStack {
Text(product.name)
.font(Font.custom("Helvetica Neue", size: 14))
.foregroundColor(Color.gray)
RatingView(starRating: product.starRating)
}
}
}
}
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};